今天早上QQ一上线,就弹出公告,说QQ与360不能共存,说实话,我也用了360,主要是用他来清清插件和垃圾,打打补丁,用完就退出,而杀毒我是用的NOD32,在我看来,杀毒软件本来就是要本着公平,公正的原则,不能因为一已之私而鼓吹自己软件是如何的好,别家的软件有这样那样的漏洞,而360做为一家底蕴不是很久的公司,把金山,瑞星,卡巴,诺顿等等世界知名,10多年的公司的杀毒软件说成是有漏洞的软件,这是一种对用户不负责任的说法,因为大多数网民根本就没有识别能力,受360一恐吓就卸载了其它的杀毒软件而装上360杀毒,说实话,360杀毒根本就是个垃圾,对,就是个垃圾,我随便写个东西就可以通过它的防线,它根本就是放过病毒,拦截正常软件的一个工具,我的一个软件更新时间的软件就被杀掉,我怀疑他们根本就没有测试的,我也想过向360安全卫士申请认证,但必须是企业会员,要交钱的,对我这种小小程序员来说,没必要,因为我的软件是免费的小软件,没必要.何况金山都被拦截,想想也觉得心理平衡了.
跑题了,其实也没有什么多说的,如果给个二换一的话,QQ是正选。我到sina.com.cn 看看评论,发现新浪也是腾讯的对手吗?我才知道,他们把反对腾讯的论点放到最上面,(声讨腾讯霸权)中间是公平的论点,而不往下面拉还看不到有反对用360的文章 ,而且不知道谁家的枪手在刷投票,二
骂马化腾的好多。好多的水军。不说了,本人观注事态发展。
编程时有时找键盘代码找半天,在Delphi中,输入键盘代码有两种方式,一种是输入VK_TAB,再另外一方法是输入数字,VK_TAB对应该10进制为9,用我这个一查就知道了。不多加说明,代码很简单,Inttostr(key)就得出来了。不好意思各位,前几天可能文件上传没完成,文件是个坏的,重新传了一下。
首先是API Hook的核心代码了,这个代码即是用于替换函数指针。
定义入口记录:
type
PIMAGE_IMPORT_ENTRY = ^TIMAGE_IMPORT_ENTRY;
TIMAGE_IMPORT_ENTRY = packed record
Characteristics: DWORD;
TimeDateStamp: DWORD;
MajorVersion: Word;
MinorVersion: Word;
Name: DWORD;
LookupTable: DWORD;
end;
这个结构的源头来自于MSDN,没有MSDN的话,也很难自己想到。当然了,如果对程序PE很有研究的话,也应该能凭经验写出这个记录来。
接下来,需要另一个记录,用于保存跳转指令和要跳转到的API函数。
type
PTIMPORT_CODE = ^TIMPORT_CODE;
TIMPORT_CODE = packed record
JmpPtr: Word;
PtrAdd: ^Pointer;
end;
其中JmpPtr保存的是导入表地址,在Delphi中可以用$25FF表示。这个值的来源,可以自己反汇编调试得到。在Call API的时候,跟踪即可,如下所示:
@@ PUSH 0
@@ MOV EAX,123
@@ CALL EAX
@@ POPFD
@@ POPAD
@@ FF25 5D108000 JMP DWORD PTR DS:[EBP]+4A
@@ NOP
注意红色的那句,将最前面那个导入表的地址,高低位互换即可。
接下来实现两个方法,一个是用于得到真实API的地址,另一个用于将自定义的函数替换掉API
function GetAPIAddress(ApiPtr: Pointer): Pointer;
begin
Result := ApiPtr;
if ApiPtr= nil then
exit;
try
if (PTIMPORT_CODE(ApiPtr).JmpPtr = $25FF) then
Result := PTIMPORT_CODE(ApiPtr).PtrAdd^;
except
Result := nil;
end;
end;
function SwapPtr(OldPtr, NewPtr: Pointer): Integer;
var
lstDosHead: TList;
function hkSwapPtr(h: Cardinal; OldPtr, NewPtr: Pointer): Integer;
var
DosHeader: PImageDosHeader;
NTHeader: PImageNTHeaders;
ImpEty: PIMAGE_IMPORT_ENTRY;
VAddr: DWORD;
Func: ^Pointer;
DLLName: string;
fOld: Pointer;
wBytes: DWORD;
begin
Result := 0;
DosHeader := Pointer(h);
if lstDosHead.IndexOf(DosHeader) >= 0 then
exit;
lstDosHead.Add(DosHeader);
OldPtr := GetAPIAddress(OldPtr);
if IsBadReadPtr(DosHeader, SizeOf(TImageDosHeader)) then
exit;
if DosHeader.e_magic <> IMAGE_DOS_SIGNATURE then
exit;
NTHeader := Pointer(Integer(DosHeader) + DosHeader._lfanew);
VAddr := NTHeader^.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
if VAddr = 0 then
exit;
ImpEty := Pointer(integer(DosHeader) + VAddr);
while ImpEty^.Name <> 0 do
begin
DLLName := PChar(Integer(DosHeader) + ImpEty^.Name);
hkSwapPtr(GetModuleHandle(PChar(DLLName)), OldPtr, NewPtr);
Func := Pointer(Integer(DosHeader) + ImpEty^.LookupTable);
while Func^ <> nil do
begin
fOld := GetAPIAddress(Func^);
if fOld = OldPtr then
begin
WriteProcessMemory(GetCurrentProcess, Func, @NewPtr, 4, wBytes);
if wBytes > 0 then
Inc(Result);
end;
Inc(Func);
end;
Inc(ImpEty);
end;
end;
begin
lstDosHead:= TList.Create;
try
Result := hkSwapPtr(GetModuleHandle(nil), OldPtr, newptr);
except
end;
lstDosHead.Free;
end;
这两步做好后,接下来的事表就很简单了,我尝试修改FindWindow这个API的处理。
type
TFindWindow = function (lpClassName, lpWindowName: PChar): HWND; stdcall;
TFindWindowA = function (lpClassName, lpWindowName: PAnsiChar): HWND; stdcall;
TFindWindowW =function (lpClassName, lpWindowName: PWideChar): HWND; stdcall;
var
OldFindWindow: TFindWindow;
OldFindWindowA: TFindWindowA;
OldFindWindowW: TFindWindowW;
function hkFindWindow(lpClassName, lpWindowName: PChar): HWND; stdcall;
begin
if lpWindowName = 'Form1' then
Result := 0
else
Result := OldFindWindow(lpClassName, lpWindowName);
end;
function hkFindWindowA(lpClassName, lpWindowName: PAnsiChar): HWND; stdcall;
begin
if lpWindowName = 'Form1' then
Result := 0
else
Result := OldFindWindowA(lpClassName, lpWindowName);
end;
function hkFindWindowW(lpClassName, lpWindowName: PWideChar): HWND; stdcall;
begin
if lpWindowName = 'Form1' then
Result := 0
else
Result := OldFindWindowW(lpClassName, lpWindowName);
end;
很明确了,接管的结果应该是,当调用FindWindow这个API时,如果lpWindowName这个参数是Form1,那么直接返回0。也就是说,不想让别的程序能找到Form1。如果找的不是Form1,那么就把控制权再交还给原先的FindWindow,让它继续执行。
下面的代码是处理用自己的函数替换掉API函数。替换过后,API就被接管了。
procedure DoHook;
begin
if @OldFindWindow = nil then
@OldFindWindow := GetAPIAddress(@FindWindow);
if @OldFindWindowA = nil then
@OldFindWindowA := GetAPIAddress(@FindWindowA);
if @OldFindWindowW = nil then
@OldFindWindowW := GetAPIAddress(@FindWindowW);
SwapPtr(@OldFindWindow, @hkFindWindow);
SwapPtr(@OldFindWindowA, @hkFindWindowA);
SwapPtr(@OldFindWindowW, @hkFindWindowW);
end;
procedure DoUnHook;
begin
if @OldFindWindow <> nil then SwapPtr(@hkFindWindow, @OldFindWindow);
if @OldFindWindowA <> nil then SwapPtr(@hkFindWindowA, @OldFindWindowA);
if @OldFindWindowW <> nil then SwapPtr(@hkFindWindowW, @OldFindWindowW);
end;
前一个函数是用于替换API,当程序结束时,为了不影响系统的正常运作,应当把替换过的API再换回正常的。
替换API的工作完成后,我们需要将它挂到系统上,这样才能够在全局范围内替换掉API。如果不进行系统级的挂勾,那么对于API的替换,只作用于当前程序本身。
挂勾到系统也很简单,如下:
var
HookHandle: Cardinal;
function GetMsgProc(code: integer; removal: integer; msg: Pointer): Integer; stdcall;
begin
Result := 0;
end;
procedure DoHookSystem;
begin
HookHandle := SetWindowsHookEx(WH_GETMESSAGE, @GetMsgProc, HInstance, 0);
end;
procedure DoUnHookSystem;
begin
UnhookWindowsHookEx(HookHandle);
end;
这里借用了GetMsgProc这个回调函数,并直接返回0,使挂勾能持续运作。
与DoHook时一样,为了使程序退出后系统不受影响,也需要一个DoUnHookSystem方法。
到此为止,一个简单的API Hook就完成了。对于Windows API的编程有些时候的确是很麻烦,必要时还得借助汇编或是Spy++之类的工具来帮助完成任务。当然了,通过此例,也可以看到,API Hook其实并不是什么非常高深的技术,凭借着对PE的了解,加上手头有个MSDN,许多问题就能迎刃而解了
最近在做的一个软件,其中有一部分功能需要调用其它的软件来完成,而那个软件只有可执行文件,根本没有源代码,幸好,我要做的事不难,只需要在我的程序启动后,将那个软件打开,在需要的时候,对其中的一个文本矿设置一些文字,再点击一个按钮就可以了。
说到这里,相信你也有了对该功能的一些初步设想了,没错,其基本思路就是:
1)调用CreateProcess()打开目标程序。
2)用FindWindow()找到目标程序的窗口Handle。
3)找到文本框的Handle,以及按钮的MessageID,用SendMessage()方法设置文字,并触发事件。
好了,这样确实很简单吧,但是当我实现它后,却发现这样做的结果则是:当我的程序启动并打开目标程序时,它的Splash窗口,以及主窗口都将显示出来,即使当我用FindWindow()找到主窗口Handle后,调用SendMessage(WindowHandle, SW_HIDE)来隐藏该窗口,还是会有一瞬主窗口被显示出来的,这样的效果实在是最求完美的我不忍心看到的。
那么怎么解决这个问题呢,首先我当然在CreateProcess()上面寻找方法,可惜,它只有一个参数可以设置窗口的默认显示方式,但是一旦这个窗口自己重设了显示方式,它就没有任何作用了。。。。继续查找文档,这时我看到CreateProcess()的一个参数TStartupInfo中有 lpDesktop这么一个属性,按照MSDN的说法,如果该指针为NULL,那么新建的Process将在当前Desktop上启动,而如果对其赋了一个Desktop的名称后,Process将在指定的Desktop上启动,恩,看来不错,就从它入手了:
1)首先,建立一个虚拟的Desktop,
- const
- DesktopName = 'MYDESK';
- FDesktop:=CreateDesktop(DesktopName,nil,nil,0,GENERIC_ALL,nil);
Windows中可以建立多个Desktop,可以使用SwitchDesktop()来切换哪个Desktop被显示出来,以前有过将Windows模拟成Linux的形式,可以在多个虚拟Desktop中切换的程序,其实那种程序也是用的Windows本身的虚拟Desktop功能来实现的,另外 Windows的启动画面,以及屏保画面也都是用虚拟Desktop实现的,好了,关于这方面不多介绍了,感兴趣的话,可以到MSDN中查看更详细资料:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dllproc/base/enumdesktops.asp
2)在CreateProcess的时候,指定程序在我新生成的Desktop上运行:
- var
- StartInfo:TStartupInfo;
- FillChar(StartInfo, sizeof(StartInfo), 0);
- StartInfo.cb:=sizeof(StartInfo);
- StartInfo.lpDesktop:=PChar(DesktopName); //指定Desktop的名称即可
- StartInfo.wShowWindow:=SW_HIDE;
- StartInfo.dwFlags:=STARTF_USESHOWWINDOW;
- StartInfo.hStdError:=0;
- StartInfo.hStdInput:=0;
- StartInfo.hStdOutput:=0;
- if not CreateProcess(PChar(FileName),nil,nil,nil,true,CREATE_NEW_CONSOLE+HIGH_PRIORITY_CLASS,nil,PChar(ExtractFilePath(FilePath)),StartInfo,FProceInfo) then begin
- MessageBox(Application.Handle,'Error when init voice (5).',PChar(Application.Title),MB_ICONWARNING);
- exit;
- end;
3)用FindWindow去找程序的主窗口
开始我直接写下了这样的代码:
- for I:=0 to 60 do begin //wait 30 seconds for open the main window
- WindowHandle:=FindWindow(nil,'WindowCaption');
- if WindowHandle<>0 then begin
- break;
- end;
- Sleep(500);
- end;
但是,实践证明,这样是找不到不在当前Desktop中的Window的,那怎么办呢:
答案是,可以用SetThreadDesktop()函数,这个函数可以设置当前Thread工作所在的Desktop,于是我在以上代码前又加了一句:
- if not SetThreadDesktop(FDesktop) then begin
- exit;
- end;
但是,程序运行后,该函数却返回了false,说明方法调用失败了,再仔细看MSDN,发现有这么一句话:
The SetThreadDesktop function will fail if the calling thread has any windows or hooks on its current desktop (unless the hDesktop parameter is a handle to the current desktop).
哦,原来需要切换Desktop的线程中不能有任何UI方面的东西,而我是在程序的主线程中调用该方法的,当然会失败拉,知道了这点就好办了,我只需要用一个“干净”的线程,让它绑定到新的Desktop上,再让它用FindWindow()方法找到我要找的WindowHandle,不就可以了吗,于是,这一步就需要借助一个线程了,线程的代码如下:
- TFindWindowThread = class(TThread)
- private
- FDesktop:THandle;
- FWindowHandle:THandle;
- protected
- procedure Execute();override;
- public
- constructor Create(ACreateSuspended:Boolean;const ADesktop:THandle);reintroduce;
- property WindowHandle:THandle read FWindowHandle;
- end;
- { TFindWindowThread }
- procedure TFindWindowThread.Execute();
- var
- I:Integer;
- begin
- //make the current thread find window on the new desktop!
- if not SetThreadDesktop(FDesktop) then begin
- exit;
- end;
- for I:=0 to 60 do begin //wait 30 seconds for open the main window
- FWindowHandle:=FindWindow(nil,PChar('WindowCaption'));
- if FWindowHandle<>0 then begin
- break;
- end;
- Sleep(500);
- end;
- end;
- constructor TFindWindowThread.Create(ACreateSuspended:Boolean;const ADesktop:THandle);
- begin
- inherited Create(ACreateSuspended);
- FDesktop:=ADesktop;
- end;
而主程序中的代码变成这样:
- FindWindowThread:=TFindWindowThread.Create(false,FDesktop);
- try
- FindWindowThread.WaitFor;
- FMainWindowHandle:=FindWindowThread.WindowHandle;
- finally
- FindWindowThread.Free;
- end;
- if FMainWindowHandle=0 then begin
- MessageBox(Application.Handle,'Error when init voice (6).',PChar(Application.Title),MB_ICONWARNING);
- exit;
- end;
呵呵,成功,这样果然可以顺利的找到窗口Handle了。
4)最后,再用这个主窗口Handle,找出里面的EditBox的Handle,如这样:
- FEditWindow:=FindWindowEx(FMainWindowHandle,0,PChar('Edit'),nil);
我在这里指定了这个文本框的ClassName,这个名称可以用Spy++得到。
初始化的工作就到此结束了,如果顺利,程序就真正在后台被运行了起来。那么功能调用呢,还是和一般的做法一样:
- if (FMainWindowHandle=0) or (FEditWindow=0) then begin
- exit;
- end;
- SendMessage(FEditWindow,WM_SETTEXT,0,LongInt(@AText[1]));
- SendMessage(FMainWindowHandle,WM_COMMAND,$8012,$0);
其中$8012这个数字,也是用Spy++来得到的资源ID。
最后,别忘了关闭程序,以及释放虚拟Desktop:
- if FProceInfo.hProcess<>0 then begin
- TerminateProcess(FProceInfo.hProcess,0);
- end;
- if FDesktop<>0 then begin
- CloseDesktop(FDesktop);
- end;
好了,这样就几乎完美的实现了一个后台调用程序的功能,它对最终客户来说将是完全透明的,客户根本感觉不到后台还有另一个程序在工作。是不是很爽啊,这样别人的很多程序我们都可以直接拿来用了(当然了,得在遵守版权的基础上才行拉)。






