API监视工具
这款工具的功能就是,我们运行一个程序的时候我们可以监视用户输入的某个API函数是否在指定程序中使用到,如果使用到了,那么将会先到我们这里来。
程序的思路
思路不是很难,就是HOOK我们的API函数,但是由于我们不知道函数的入栈规则(因为API是用户输入的,参数类型和个数是不确定的),所以我们只能编写一小段不占用堆栈的函数,然后跳转到我们真正的函数的(并不是说不调用,而是在内部CALL的时候可以使用,但是要保证堆栈的完全平衡)。但是如何让我们的CAPIHook类知道我们要监视的函数呢,这个就要用到我们的CShareMemory类,这个类在我之前的博客写过。
DLL注入
这里因为我们要专门注入某个进程,所以使用尽量能简单写就简单写,索引这里我们就用远程注入进行注入DLL。
DLL注入的简单区分
这里简单了解一下为什么要学习多种DLL注入(不包括那些乱七八糟的注入,什么注册表…的)
- 远程注入DLL,这种注入比较好用,在用OD注入的时候也经常用到,这个注入的思想就是和我们写Call基本上是一模一样,但是有一点区别就是说,我们这里的函数地址是根据Windows版本不同自动变化的,简单说就是由于
KERNEL32.DLL
映射在地址空间中的时候在所有的进程中的地址一样,并且有一点很重要,LoadLibraryA
这个函数的入栈和其他的方式和我们调用远程线程的线程回掉函数一模一样,所以这就暗示我们可以直接在别的进程加载DLL。
- 为什么远程注入这么好用我们还需要学这个钩子注入方法?其实很明显就是说你会发现我们的远程注入是存在一个缺陷的,就是说他只能注入某个进程,但是钩子就不一样了,他可以监控整个系统,导致我们可以注入整个系统(需要进程有钩子)。
远程注入的优点:
- 调用起来稳定
- 简单实用,而且DLL里面只需要有我们需要的函数
远程注入的缺点:
钩子注入的优点:
钩子注入的缺点:
- 需要导出DLL中的钩子函数
- 目标进程必须存在某种行为,也就是钩子所需要的监视过程(一般用消息循环,但是窗口程序确实没有)
远程注入
之前已经说过了钩子注入,这里说一下远程注入。
头文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <Windows.h>
class CRemThreadInjector { public: CRemThreadInjector(LPCTSTR pszDllName); ~CRemThreadInjector();
BOOL InjectModuleInto(DWORD dwProcessId,BOOL bInject = TRUE);
protected: char m_szDllName[MAX_PATH]; static BOOL EnableDebugPrivilege(BOOL bEnable); };
|
声明的话呢就一个构造函数,一个析构函数和一个注入函数,除此之外还有一个静态函数,这个静态函数是比较特殊的。
EnableDebugPrivilege
这个函数是一个提权函数,因为某些比较高权限的程序我们是注入不进去,就比如下PID较低的函数是不能注入的,所以我们需要提升我们的权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| BOOL CRemThreadInjector::EnableDebugPrivilege(BOOL bEnable) { BOOL bOk = FALSE; HANDLE hToken; if (OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES,&hToken)) { LUID uID; LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&uID); TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; tp.Privileges[0].Luid = uID; tp.Privileges[0].Attributes = bEnable?SE_PRIVILEGE_ENABLED:0; AdjustTokenPrivileges(hToken,FALSE,&tp,sizeof(tp),NULL,NULL); bOk = (GetLastError() == ERROR_SUCCESS); CloseHandle(hToken); } return bOk; }
|
传进来一个是否是提权还是降权,首先我们鲜活的我们当前的进程Token,然后再获取我们的UID,获取之后申请一个权限变量:TOKEN_PRIVILEGES tp;
,然后将我们的安全属性设置为:SE_PRIVILEGE_ENABLED或0(提权或者降权),最后再调整,关掉句柄。
在构造函数和析构函数中我们作如下操作:
1 2 3 4 5 6 7 8 9 10
| CRemThreadInjector::CRemThreadInjector(LPCTSTR pszDllName) { strncpy(m_szDllName,pszDllName,MAX_PATH); EnableDebugPrivilege(TRUE); }
CRemThreadInjector::~CRemThreadInjector() { EnableDebugPrivilege(FALSE); }
|
就是一个字符串拷贝和提权降权。
核心的注入代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| BOOL CRemThreadInjector::InjectModuleInto(DWORD dwProcessId,BOOL bInject) { if (GetCurrentProcessId() == dwProcessId) return FALSE;
BOOL bFound = FALSE; MODULEENTRY32 me32 = {0}; HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,dwProcessId); me32.dwSize = sizeof(MODULEENTRY32); if (Module32First(hModuleSnap,&me32)) { do { if (lstrcmpiA(me32.szExePath,m_szDllName) == 0) { bFound = TRUE; break; } } while (Module32Next(hModuleSnap,&me32));
} if (bInject == bFound) return FALSE;
HANDLE hProcess = OpenProcess(PROCESS_VM_WRITE | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION ,FALSE,dwProcessId); if(hProcess == NULL) return FALSE;
HMODULE hModule = GetModuleHandle("KERNEL32.DLL");
LPTHREAD_START_ROUTINE pfnStartRoutine; HANDLE hRemoteThread ; if(bInject) { int cbSize = (strlen(m_szDllName)+1); LPVOID lpRemoteDllName = VirtualAllocEx(hProcess,NULL,cbSize,MEM_COMMIT , PAGE_EXECUTE_READWRITE); WriteProcessMemory(hProcess,lpRemoteDllName,m_szDllName,cbSize,NULL); pfnStartRoutine = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule,"LoadLibraryA"); hRemoteThread = CreateRemoteThread(hProcess,NULL,0,pfnStartRoutine,lpRemoteDllName,0,NULL); } else { pfnStartRoutine = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule,"FreeLibrary"); hRemoteThread = CreateRemoteThread(hProcess,NULL,0,pfnStartRoutine,me32.hModule,0,NULL); }
if (hRemoteThread == NULL) { CloseHandle(hProcess); return FALSE; }
WaitForSingleObject(hRemoteThread,INFINITE);
CloseHandle(hRemoteThread); CloseHandle(hProcess); return TRUE; }
|
首先先是判断我们是否有加载或者没有加载这个模块,主要是通过传过来的第二个BOOL类型的值判断,然后我们获取函数的地址。然后我们申请一小块内存用来存放我们的加载模块的全路径,然后调用远程线程去执行这个函数,等待我们的线程执行完毕然后关掉我们的句柄。
内存共享
因为我们需要传递我们的DLL注入,所以我们需要现在内存中共享一下我们的API函数和模块名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #ifndef __APISPYLIB_H_ #define __APISPYLIB_H_ #include "CShareMemory.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 bSever = FALSE) { m_pMem = new CShareMemory(SHAREMEM,sizeof(CAPISpyData),bSever); m_pData = (CAPISpyData*)(m_pMem->GetBuffer()); if(m_pData == NULL) ExitProcess(-1); } ~CMyShareMem(){delete m_pMem;} CAPISpyData* GetData() const {return m_pData;} private: CAPISpyData* m_pData; CShareMemory* m_pMem; };
#endif
|
我们包含之前写的内存共享类(内存映射),然后用一个结构体来保存我们的关键内容,模块名,函数名和窗口句柄。构造函数也就是初始化一下内存共享类,然后将我们的结构放在我们共享内存的位置。
DLL编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
#include "stdafx.h" #include "CMyShareMem.h" #include "CAPIHook.h"
void HookProc();
CMyShareMem g_shareData(FALSE);
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); }
__declspec(naked)void HookProc() { NotifyCaller();
DWORD dwOrgAddr; dwOrgAddr = (DWORD)PROC(g_orgFun); __asm { mov eax,dwOrgAddr jmp eax } }
|
理解起来不是很难,我们先构造内存共享,只不过我们这边是接收端,还有我们APIHook类,这之前博客说过了。然后定义一个通知函数,用来通知窗口我们调用了,在我们替换的函数中,因为我们不确定API函数是什么样子的堆栈结构,所以我们这里用naked函数调用,也就是不进行堆栈平衡的调节。,这个函数最关键的一步就是先发送信息,然后我们获取我们原先函数的位置,PROC这个操作符我们在CAPIHook类已经定义过了,获取之后跳转过去就好,这里我们先要通过EAX做间接变量(我记得VS编译器不能call和jmp一个内存地址,只能这么中转)。
主程序编写
初始化对话框的时候:
1 2 3 4 5 6 7 8
| TCHAR strName[256] = {0}; GetModuleFileName(NULL,strName,256); CString FullPath; FullPath.Format("%s",strName); FullPath = FullPath.Left(FullPath.ReverseFind('\\'));
m_pInjector = new CRemThreadInjector(FullPath+"\\APISpyLib.dll"); m_pShareMem = NULL;
|
也就是先获取当前运行的目录,然后和我们的DLL进行拼接(测试的时候可以直接输入文件路径)。
然后初始换我们的成员变量。
消息接收
1 2 3 4 5
| afx_msg LRESULT CAPISpyEXEDlg::OnHmSpyacall(WPARAM wParam, LPARAM lParam) { MessageBox("检测用到这个函数了!!!"); return 0; }
|
这里我们直接弹出一个信息框就好了。
开始监视按钮:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| void CAPISpyEXEDlg::OnBnClickedOnstart() { CString strTargetApp; GetDlgItem(IDC_TARGETAPP)->GetWindowText(strTargetApp); if (strTargetApp.IsEmpty()) { MessageBox("请输入目标程序"); return; }
CString strAPIName,strDllName; GetDlgItem(IDC_APINAME)->GetWindowText(strAPIName); GetDlgItem(IDC_DLLNAME)->GetWindowText(strDllName);
if (strAPIName.IsEmpty() || strDllName.IsEmpty()) { MessageBox("请输入要侦查的文件名"); return; }
HMODULE hdll = ::LoadLibrary(strDllName); if (GetProcAddress(hdll,strAPIName) == NULL) { MessageBox("这个API不在这个模块里啊"); if(hdll != NULL) FreeLibrary(hdll); return ; } FreeLibrary(hdll);
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);
STARTUPINFO si = {sizeof(si)}; PROCESS_INFORMATION pi; BOOL bOk = CreateProcess(NULL,strTargetApp.GetBuffer(0),NULL,NULL,FALSE,0,NULL,NULL,&si,&pi); if (bOk) { bOk = m_pInjector->InjectModuleInto(pi.dwProcessId); if(!bOk) MessageBox("DLL注入失败"); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } else { MessageBox("启动目标进程失败"); }
if (!bOk) { delete m_pShareMem; m_pShareMem = NULL; return; }
}
|
首先我们先检查是否有输入,然后判断是否在这个DLL中有指定的API,然后再释放。初始化我们的内存共享类,我们这边是发送端,所以我们要初始化我们的结构体信息,然后启动我们的指定用程序,启动成功之后我们就立马注入我们的DLL,然后关闭句柄简单的盘一下就好了。需要注意的是我们需要关掉我们启动时候的句柄,否则程序就算关闭也不会完全消失,因为内核对象还在内存中。
这里可以看到是我们的信息框先出来,我们关闭的时候才会弹出他的信息框,因为DLL那边在等我们的消息返回值。