在目标程序窗口下填入要侦测的程序的文件名,如果该程序在系统(system32)目录下,则可以不带路径。将要侦测的API名称和这个API所在模块的名称分别填到相应窗口下,单击“开始”按钮,09APISpyApp便创建目标进程,开始侦测它对指定API的调用。当调用发生时,将它截获,显示在左边的输出窗口。
系统自带记事本程序的文件名为notepad.exe,在修改了编辑的内容退出程序时,它便调用MessageBoxW函数询问用户是否保存对文件的修改。上图显示的是截获这个调用之后,09APISpyApp程序的输出。
这个程序的关键代码在09APISpyLib.dll模块中。用户单击“开始”按钮请求侦测时,09APISpyApp程序将用户输入的参数写入共享内存,然后创建目标进程,并将09APISpyLib.dll模块注入。09APISpyLib.dll模块在初始化期间从共享内存取得要侦测的API函数和这个API所在的模块,然后使用自定义函数HookProc替换要侦测的API函数,使目标进程对这个API的调用都变成对HookProc的调用。HookProc函数先通知09APISpyApp程序调用已经发生,然后跳转到原API地址处去执行。
1.共享内存
主程序与DLL使用共享内存来传输数据。09APISpyLib.dll在初始化时读共享内存中的数据,以便知道要挂钩哪个API函数。为了使这一过程方便进行,程序在CShareMem类的基础上又封装了一个CMyShareMem类。下面是APISpyLib.h文件中的内容,里面包含共享内存数据结构和CMyShareMem类的定义。
// APISpyLib.h文件
#ifndef __APISPYLIB_H__
#define __APISPYLIB_H__
#include "ShareMemory.h"
// 共享内存名称
#define SHAREMEM "APISpyLib"
// 发送给主程序的通知消息
#define HM_SPYACALL WM_USER + 102
struct CAPISpyData
{
char szModName[256];
char szFuncName[256];
HWND hWndCaller;
};
class CMyShareMem
{
public:
CMyShareMem(BOOL bServer = FALSE)
{
m_pMem = new CShareMemory("APISpyLib", sizeof(CAPISpyData), bServer);
m_pData = (CAPISpyData*)(m_pMem->GetBuffer());
if(m_pData == NULL) // 没有设置共享内存?
::ExitProcess(-1);
}
~CMyShareMem() { delete m_pMem; }
// 取得共享内存中的数据
CAPISpyData* GetData() { return m_pData; }
private:
CAPISpyData* m_pData;
CShareMemory* m_pMem;
};
#endif // __APISPYLIB_H__
09APISpyLib使用CAPIHook类来挂钩API,代理函数名称为HookProc。下面是在挂钩API时从共享内存中读数据的代码(在APISpyLib.cpp文件中)。
void HookProc();
// 共享内存数据。以便初始化下面的CAPIHook对象
CMyShareMem g_shareData(FALSE);
// HOOK主程序指定的API函数
CAPIHook g_orgFun(g_shareData.GetData()->szModName, g_shareData.GetData()->szFuncName, (PROC)HookProc);
2.代理函数
挂钩API时,要求代理函数的签名必须与原API相同主要是为了维持堆栈平衡。例如语句“MessageBoxA(0, “hello”, “demo”, MB_OK)”对应的汇编代码如下。
push 0
push offset string "demo" (00420024)
push offset string "hello" (0042001c)
push 0
call dword ptr [__imp__MessageBoxA@16 (004252b4)]
调用API前先将各参数压入堆栈,在MessageBoxA返回时,程序将自动清栈,即自动将压到堆栈中的参数弹出以维持堆栈平衡。如果代理函数与原来API函数参数个数不同,那么在代理函数返回时从堆栈弹出的参数就与调用发生时压入堆栈的参数的个数不同了,这绝对会使程序崩溃(因为函数返回到了一个错误的地址处)。
但是因为预先不知道要挂钩哪个API函数,所以没有办法为这个API编写一个签名完全相同的代理函数。解决此问题的办法是用"纯粹"的asm(汇编)函数作为代理函数,在代理函数中不要对堆栈进行任何操作,执行完附加代码之后,通过JUMP CPU指令直接跳转到原API地址处。为了让编译器产生这样的函数,在函数定义前加"__declspec(naked)"修饰符即可,本例中这个函数是HookProc。
// APISpyLib.cpp文件
#include <windows.h>
#include "APIHook.h"
#include "APISpyLib.h"
void HookProc();
// 共享内存数据,以便初始化下面的CAPIHook对象
CMyShareMem g_shareData(FALSE);
// HOOK主程序指定的API函数
CAPIHook g_orgFun(g_shareData.GetData()->szModName,
g_shareData.GetData()->szFuncName, (PROC)HookProc);
void NotifyCaller()
{
CMyShareMem mem(FALSE);
::SendMessage(mem.GetData()->hWndCaller, HM_SPYACALL, 0, 0);
}
// 使用这个函数替代要HOOK的API
__declspec(naked)void HookProc()
{
// 通知主程序
NotifyCaller();
// 跳转到原来的函数
DWORD dwOrgAddr;
dwOrgAddr = (DWORD)PROC(g_orgFun);
__asm
{
mov eax, dwOrgAddr;
jmp eax;
}
// 永远运行不到这里
}
如果不加"__declspec(naked)“修饰符,函数要在堆栈中保存ebp、ebx等寄存器的值,也要在堆栈中保存所有临时变量的值,在函数返回之前再将堆栈恢复。加入”__declspec(naked)"修饰符之后,编译器不会对函数做任何堆栈处理,需要自己来(如果有必要)取输入参数、保存寄存器、甚至自己写ret返回指令等。
在HookProc函数中用户定义的临时变量都被编译器保存在寄存器中(不使用堆栈)。
3.09APISpyApp工程
09APISpyApp程序负责与用户交互、创建共享内存和注入DLL。
程序在CMainDialog类(主窗口类)的构造函数中取得09APISpyLib.dll完整的文件名,创建CRemThreadInjector对象。
CMainDialog::CMainDialog(CWnd* pParentWnd):CDialog(IDD_MAIN, pParentWnd)
{
char szDllPath[MAX_PATH];
LPSTR p;
// 取得指定文件的完整文件名,将之返回到szDllPath所指的缓冲区中。p用来返回目录中最后的文件名
::GetFullPathName("09APISpyLib.dll", MAX_PATH, szDllPath, &p);
// 调试时可以直接用“E:\\MyWork\\book_code\\09APISpyLib\\Debug\\09APISpyLib.dll”作为DLL文件名
m_pInjector = new CRemThreadInjector(szDllPath);
m_pShareMem = NULL;
}
用户单击“开始”或者“停止”按钮以后,程序注入DLL或者取消注入。
// APISpyApp.cpp文件
#include <afxdlgs.h> // 为了使用CFileDialog类
#include "resource.h"
#include "APISpyApp.h"
CMyApp theApp;
BOOL CMyApp::InitInstance()
{
CMainDialog dlg;
m_pMainWnd = &dlg;
dlg.DoModal();
return FALSE;
}
CMainDialog::CMainDialog(CWnd* pParentWnd):CDialog(IDD_MAIN, pParentWnd)
{
char szDllPath[MAX_PATH];
LPSTR p;
::GetFullPathName("09APISpyLib.dll", MAX_PATH, szDllPath, &p);
// 调试时可以直接用“E:\\MyWork\\book_code\\09APISpyLib\\Debug\\09APISpyLib.dll”
m_pInjector = new CRemThreadInjector(szDllPath);
m_pShareMem = NULL;
}
CMainDialog::~CMainDialog()
{
delete m_pInjector;
}
BEGIN_MESSAGE_MAP(CMainDialog, CDialog)
ON_BN_CLICKED(IDC_BROWSER, OnBrowser)
ON_BN_CLICKED(IDC_START, OnStart)
ON_BN_CLICKED(IDC_CLEAR, OnClear)
ON_MESSAGE(HM_SPYACALL, OnSpyACall)
END_MESSAGE_MAP()
BOOL CMainDialog::OnInitDialog()
{
CDialog::OnInitDialog();
SetIcon(theApp.LoadIcon(IDI_MAIN), FALSE);
return TRUE;
}
void CMainDialog::OnCancel()
{
CDialog::OnCancel();
}
void CMainDialog::OnBrowser()
{
CFileDialog file(TRUE);
// 显示选择文件对话框
if(file.DoModal() == IDOK)
{
GetDlgItem(IDC_TARGETAPP)->SetWindowText(file.GetPathName());
}
}
void CMainDialog::OnStart()
{
// 如果m_pShareMem不为NULL,则证明用户单击“停止”按钮
if(m_pShareMem != NULL)
{
// 取消注入
m_pInjector->EjectModuleFrom(m_dwProcessId);
// 删除对象
delete m_pShareMem;
m_pShareMem = NULL;
// 设置UI界面
GetDlgItem(IDC_START)->SetWindowText("开始");
return;
}
// 取得用户输入
// 取得目标程序名称
CString strTargetApp;
GetDlgItem(IDC_TARGETAPP)->GetWindowText(strTargetApp);
if(strTargetApp.IsEmpty())
{
MessageBox("请选择目标程序!");
return;
}
// 取得API函数和所在模块名称
CString strAPIName, strDllName;
GetDlgItem(IDC_APINAME)->GetWindowText(strAPIName);
GetDlgItem(IDC_DLLNAME)->GetWindowText(strDllName);
if(strAPIName.IsEmpty() || strDllName.IsEmpty())
{
MessageBox("请输入您要侦测的函数或模块名称!");
return;
}
// 检查用户输入的函数是否在指定模块中
HMODULE h = ::LoadLibrary(strDllName);
if(::GetProcAddress(h, strAPIName) == NULL)
{
MessageBox("您输入的模块中不包含要侦测的函数!");
if(h != NULL)
::FreeLibrary(h);
return;
}
::FreeLibrary(h);
// 注入DLL
// 创建共享内存,写入参数信息
m_pShareMem = new CMyShareMem(TRUE);
m_pShareMem->GetData()->hWndCaller = m_hWnd;
strncpy(m_pShareMem->GetData()->szFuncName, strAPIName, 56);
strncpy(m_pShareMem->GetData()->szModName, strDllName, 56);
// 创建目标进程
BOOL bOK;
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
bOK = ::CreateProcess(NULL, strTargetApp.GetBuffer(0),
NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
if(bOK)
{
m_dwProcessId = pi.dwProcessId;
// 注入DLL
bOK = m_pInjector->InjectModuleInto(m_dwProcessId);
if(!bOK)
{
MessageBox("注入DLL出错!");
}
::CloseHandle(pi.hThread);
::CloseHandle(pi.hProcess);
}
else
{
MessageBox("启动目标程序失败!");
}
// 如果没有成功,删除共享内存
if(!bOK)
{
delete m_pShareMem;
m_pShareMem = NULL;
return;
}
// 设置UI界面
OnClear();
GetDlgItem(IDC_START)->SetWindowText("停止");
}
long CMainDialog::OnSpyACall(WPARAM wParam, LPARAM lParam)
{
BOOL bPause = ((CButton*)GetDlgItem(IDC_CHECKPAUSE))->GetCheck();
if(bPause)
return 0;
CString strItem ;
strItem.Format(" 第%d次调用; \r\n", m_nCount++);
// 添加到编辑框中
CString strEdit;
GetDlgItem(IDC_SPYMSG)->GetWindowText(strEdit);
GetDlgItem(IDC_SPYMSG)->SetWindowText(strItem + strEdit);
return 0;
}
void CMainDialog::OnClear()
{
m_nCount = 0;
GetDlgItem(IDC_SPYMSG)->SetWindowText("");
}