最近一直在做沙箱項目,在項目快接近結尾的時候,我想給在我們沙箱中運行的程序界面打上一個標記——標識其在我們沙箱中運行的。我大致想法是:在被注入程序的頂層窗口上方顯示一個“標題性”窗口,頂層窗口外框外顯示一個“異形”的空心窗口。這些窗口如影子般隨著其被“吸附”窗口移動而移動,大小變化而變化。(轉載請指明出處)以記事本為被注入程序為例:
我用的注入和HooKApi方案是采用微軟的detour庫。關於如何HookApi的方法,可以參看我之前的《一種注冊表沙箱的思路、實現——Hook Nt函數》。注入的方案,我采用的Detour庫的DetourCreateProcessWithDll函數,該函數的W版原型是
[cpp]
BOOL WINAPI DetourCreateProcessWithDllW(LPCWSTR lpApplicationName,
__in_z LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation,
LPCSTR lpDllName,
PDETOUR_CREATE_PROCESS_ROUTINEW pfCreateProcessW)
該函數從倒數第二個參數之前的全是CreateProcessW的參數,倒數第二個參數是我們要注入的DLL的路徑,最後一個參數是真實的CreateProcessW的函數入口地址。該函數的實現細節是:
1 以掛起的方式啟動被注入程序
2 在內存中,修改被注入程序的導入表信息,在表中增加一個我們要注入的DLL中的導出函數
3 恢復被掛起的進程
該方案通過修改程序導入表,讓系統誤以為該程序需要調用到我們要注入的DLL中的導出函數,於是將我們注入的DLL加載到該進程內存空間,從而實現注入。這兒有個細節要說明:該方案要求我們注入DLL要至少有一個導出函數,哪怕這個函數什麼也不做。
源碼中RegSandBoxMainDialog工程是個MFC工程,它用於啟動我們注入的進程並實現注入。我們查看其注入代碼的實現
[cpp]
UpdateData(TRUE);
char chCurrentPath[MAX_PATH] = {0};
GetModuleFileNameA( NULL, chCurrentPath, MAX_PATH );
std::string wszCurrentPath = chCurrentPath;
size_t nindex = wszCurrentPath.rfind('\\');
wszCurrentPath = wszCurrentPath.substr( 0, nindex );
wszCurrentPath += "\\";
std::string szDllPath = wszCurrentPath;
szDllPath += "HookWindow.dll"; // 拼接處注入DLL的完整路徑
std::wstring wszFilePath = m_Path; // 被注入進程的路徑
STARTUPINFO StartupInfo;
ZeroMemory(&StartupInfo, sizeof(STARTUPINFO));
StartupInfo.cb = sizeof(STARTUPINFO);
PROCESS_INFORMATION ProcessInfo;
ZeroMemory(&ProcessInfo, sizeof(PROCESS_INFORMATION));
// FL:使用DetourCreateProcessWithDll需要注入的DLL要有一個導出函數
BOOL bSuc = DetourCreateProcessWithDll( wszFilePath.c_str(), NULL, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE ,
NULL, NULL, &StartupInfo, &ProcessInfo, szDllPath.c_str(), CreateProcessW );
HookWindow工程編譯連接處HookWIndow.dll,我將在之後一步一步介紹這個DLL的編寫。
HookWindow是一個Win32 dll工程,我們為其定義一個def文件HookWindow.def,其內容為:
[plain]
LIBRARY "HookWindow"
EXPORTS Notify
Notify函數是為了達到DetourCreateProcessWithDll要求:注入DLL必須要至少有一個導出函數(原因已在上面說明過)而設計的,實際這個函數什麼也沒做,他就是個空殼。
[plain]
VOID Notify(){
}
現在得開始考慮窗口的實現了。我們知道windows系統是消息驅動的模型,那麼我們的“吸附”窗口的消息模型該是什麼樣的?當時我思考方案時得出以下兩種方案:
1 Hook進程內窗口消息,在消息鏈中根據頂層窗口消息而決定我們窗口的創建、顯示、隱藏和銷毀。這相當於我們窗口的消息循環使用了被注入進程的頂層窗口的消息循環。
2 注入進程後,啟動一個線程,該線程負責創建窗口,同時在該線程中再啟動一個監視被注入進程頂層窗口的線程,該線程將根據其得到的被注入進程窗口的位置大小狀態等信息告訴我們窗口應該做何種處理。
這兩種方法各有其優缺點,方法1比方法2少1個線程,但是存在一種場景:當點擊被注入程序頂層窗口的非客戶區時,我們的窗口會被蓋掉,因為這個時候還沒輪到我們窗口處理該消息(SetWIndowsHookEx WH_CALLWNDPROCRET),此時我們無法讓我們窗口顯示在被注入進程頂層窗口前面。方法2就是比方法1多出線程數,如果我想創建兩個窗口,就多出兩個窗口線程,以此類推。如我設想的需求,我將創建一個管理外框異形空心窗口的線程和一個“標題”窗口,那就多出兩個線程。
我覺得我這兩個窗口要處理的消息非常簡單,同樣也想做點與眾不同。於是我設計了這樣的方案,方案是融合了方案1和方案2的優點:
SetWindowsHookEx勾住被注入進程的消息,同時設置Hook類型為WH_CALLWNDPROCRET。
[cpp]
VOID HookWindowsFn()
{
do {
g_hhook = SetWindowsHookEx( WH_CALLWNDPROCRET, CallWndRetProc, NULL, GetCurrentThreadId() );
if ( NULL == g_hhook ) {
_ASSERT(FALSE);
}
InitializeCriticalSection( &g_cs );
} while (0);
}
這樣我們將在原程序處理完消息後進行消息處理。
[cpp]
LRESULT CALLBACK CallWndRetProc( __in int nCode, __in WPARAM wParam, __in LPARAM lParam )
{
if ( NULL != lParam ) {
LPCWPRETSTRUCT lptagCWPRETSTRUCT = (LPCWPRETSTRUCT)lParam;
DealMsg( lptagCWPRETSTRUCT->hwnd, lptagCWPRETSTRUCT->message, lptagCWPRETSTRUCT->wParam, lptagCWPRETSTRUCT->lParam );
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
當我們收到消息時,我們要判斷是否是我們關心的消息,這樣將減少我們處理消息的線程的工作量。
[cpp]
BOOL IsNeedDealMsg( UINT uMsg )
{
return ( IsNeedShowMsg( uMsg )
|| ( WM_DESTROY == uMsg )
|| ( WM_CLOSE == uMsg ) );
}
BOOL IsNeedShowMsg( UINT uMsg )
{
return ( ( WM_SHOWWINDOW == uMsg )
|| ( WM_MOVE == uMsg )
|| ( WM_MOVING == uMsg )
|| ( WM_SIZE == uMsg )
|| ( WM_WINDOWPOSCHANGED == uMsg )
);
}
[cpp]
VOID DealMsg( HWND hAttachedWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
if ( FALSE == IsNeedDealMsg( uMsg ) ) {
return;
}
其次判斷該窗口是否為我們自己創建的“吸附”窗口。如果是我們的“吸附”窗口,我們將不會做任何處理。
[cpp]
VOID DealMsg( HWND hAttachedWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
if ( IsHelperWindow( hAttachedWnd ) ) {
return;
}
[cpp] view plaincopy
BOOL IsHelperWindow( HWND hwnd )
{
WCHAR wszClassNameBuffer[MAX_PATH] = {0};
int nClassNameLength = GetClassName( hwnd , wszClassNameBuffer, MAX_PATH - 1 );
if ( 0 != nClassNameLength ) {
std::wstring wszClassName = wszClassNameBuffer;
if ( 0 == wcscmp( wszClassNameBuffer, TITILEWINDOWCLASS ) ||
0 == wcscmp( wszClassNameBuffer, OUTSIDEWINDOWCLASS ) ) {
return TRUE; // 通過類名判斷
}
}
return FALSE;
}
然後我們需要根據消息類型,對窗口句柄做個判斷。因為如果我們“宿主”窗口處理完WM_DESTROY後,我們再將不能對其調用GetWindowLong以獲取其樣式。於是對WM_DESTORY消息,我們只是判斷其是否為頂層窗口。如果不是該消息,我們將判斷該窗口是否為頂層窗口,且其窗口樣式包含WS_SYSMENU(我試驗了下,我所遇到的我認為該處理的窗口都有該屬性,這個屬於經驗之談,不一定准確)。
[cpp]
if ( WM_DESTROY != uMsg ) {
if ( FALSE == IsValibleWindow( hAttachedWnd ) ) {
return;
}
}
else {
if ( FALSE == IsBaseWindow(hAttachedWnd) ) {
return;
}
}
if ( FALSE == IsNeedDealMsg( uMsg ) ) {
return;
}
[cpp]
BOOL IsValibleWindow( HWND hWnd )
{
if ( FALSE == IsBaseWindow( hWnd ) )
{
return FALSE;
}
DWORD dwStyle = ::GetWindowLong( hWnd, GWL_STYLE );
if ( !( WS_SYSMENU & dwStyle ) ) {
return FALSE;
}
return TRUE;
}
BOOL IsTopWindow( HWND hwnd )
{
BOOL bTop = FALSE;
do {
HWND hParentHwnd = NULL;
HWND hParenthwnd = GetParent( hwnd );
if ( NULL == hParenthwnd ){
bTop = TRUE;
}
} while (0);
return bTop;
}
BOOL IsBaseWindow( HWND hWnd )
{
if ( FALSE == ::IsWindow(hWnd)
|| FALSE == IsTopWindow(hWnd) )
{
return FALSE;
}
return TRUE;
}
如果是原程序創建的窗口,則判斷該句柄是否已經存在一個管理“吸附”窗口的線程(該信息保存在一個Map中)。如果不存在,就創建一個管理兩個“吸附”窗口的線程,並將<HWND,HTHREADHANDLE>對保存到Map中。如果存在,則向這個線程管理的窗口發送相應的消息。一個進程可能不止是存在一個頂層窗口,所以我這兒要建立Map信息。
[cpp]
typedef struct _WindowThreadColloction_{
LPCWindowThread lpTitleWindowThread;
}WindowThreadColloction, *pWindowThreadColloction;
typedef std::map<HWND,WindowThreadColloction> MapHwndThread;
typedef MapHwndThread::iterator MapHwndThreadIter;
[cpp]
LPCWindowThread lpWindowThread = NULL;
lpWindowThread = GetTitleWindowThread( hAttachedWnd );
if ( NULL == lpWindowThread ) {
if ( WM_SHOWWINDOW == uMsg ) {
lpWindowThread = new CWindowThread(hAttachedWnd);
if ( NULL == lpWindowThread ) {
_ASSERT(FALSE);
return;
}
UpdateHwndTitleWindowThread( hAttachedWnd, lpWindowThread );
}
else {
return;
}
}
lpWindowThread->NotifyMsg( uMsg );
}
現在我們將看一下我們管理兩個“吸附”窗口的線程類。
[cpp]
class CWindowThread:
public CMessageLoop,
public CMessageFilter
{
public:
CWindowThread(void);
~CWindowThread(void);
public:
CWindowThread(HWND hAttachWindow);
public:
VOID NotifyMsg(UINT uMsg);
VOID ExitThread();
BOOL PreTranslateMessage(MSG* pMsg);
private:
static DWORD WINAPI ThreadRoutine(LPVOID lpParam);
private:
HWND m_hAttachWindow;
HANDLE m_hThread;
CWTLTitleWindow* m_pCWTLTitleWindow;
CWTLOutSideWindow* m_pCWTLOutSideWindow;
};
typedef CWindowThread* LPCWindowThread;
消息循環是在該線程中的,於是繼承於CMessageLoop;因為我們要讓我們窗口屏蔽ATL+F4這類的操作,所以我們要PreTranslateMessage,於是要繼承於CMessageFilter。
[cpp]
BOOL CWindowThread::PreTranslateMessage( MSG* pMsg )
{
if ( WM_SYSKEYDOWN == pMsg->message ) {
return TRUE;
}
return FALSE;
}
[cpp]
DWORD WINAPI CWindowThread::ThreadRoutine( LPVOID lpParam )
{
CWindowThread* pThis = (CWindowThread*) lpParam;
if ( NULL == pThis ){
return 0xFFFFFFFF;
}
if ( NULL == pThis->m_pCWTLTitleWindow ) {
pThis->m_pCWTLTitleWindow = new CWTLTitleWindow( pThis->m_hAttachWindow );
pThis->m_pCWTLTitleWindow->Create( pThis->m_hAttachWindow ); // 注意這兒要設置為父窗口
pThis->m_pCWTLTitleWindow->RunWindowMsgLoop();
}
if ( NULL == pThis->m_pCWTLOutSideWindow ) {
pThis->m_pCWTLOutSideWindow = new CWTLOutSideWindow( pThis->m_hAttachWindow );
pThis->m_pCWTLOutSideWindow->Create( pThis->m_hAttachWindow ); // 注意這兒要設置為父窗口
pThis->m_pCWTLOutSideWindow->RunWindowMsgLoop();
}
pThis->Run(); // 啟動消息循環
return 0;
}
[cpp]
VOID CWindowThread::NotifyMsg( UINT uMsg )
{
if ( m_pCWTLTitleWindow ){
m_pCWTLTitleWindow->DealMsg( uMsg );
}
if ( m_pCWTLOutSideWindow ) {
m_pCWTLOutSideWindow->DealMsg( uMsg );
}
}
下面再來看看窗口的實現。因為我們要做的是“吸附”窗口,該窗口應該不能影響原窗口正常的行為(比如不應該搶焦點,不在任務欄出現),同時考慮到刷新問題,我們要讓該窗口具有雙緩存。以“標題”窗口為例
[cpp]
class CWTLTitleWindow:
public CDoubleBufferWindowImpl< CWTLTitleWindow, CWindow, CWinTraits<WS_POPUP|WS_CLIPSIBLINGS, WS_EX_LEFT|WS_EX_LTRREADING|WS_EX_NOPARENTNOTIFY|WS_EX_NOACTIVATE>>
{
public:
typedef CWTLTitleWindow _thisClass;
typedef CDoubleBufferImpl<_thisClass> _baseDblBufImpl;
DECLARE_WND_CLASS_EX(TITILEWINDOWCLASS, CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS, COLOR_WINDOW);
CWTLTitleWindow(void);
~CWTLTitleWindow(void);
public:
CWTLTitleWindow(HWND hAttachWindow);
BEGIN_MSG_MAP_EX(CWTLTitleWindow)
MESSAGE_HANDLER( WM_SHOWWINDOW, OnShow )
MESSAGE_HANDLER( WM_DESTROY, OnDestroy)
MESSAGE_HANDLER( WM_QUIT, OnQuit )
MESSAGE_HANDLER( WM_MOUSEACTIVATE, OnMouseActive )
MESSAGE_RANGE_HANDLER( WM_USER, WM_USER + WM_USER, OnDealUserMsg )
CHAIN_MSG_MAP(_baseDblBufImpl)
END_MSG_MAP()
LRESULT OnShow(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
LRESULT OnQuit(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
LRESULT OnMouseActive(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
LRESULT OnDealUserMsg(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
void DoPaint(HDC dc);
VOID Start();
VOID DealMsg( UINT uMsg );
private:
ECalcResult CalcTitleWindowXY( int& x, int& y );
VOID ShowWindow();
private:
HWND m_hAttachHWnd;
HINSTANCE m_hInstance;
HBITMAP m_hBitmap;
RECT m_hAttachWindowRect;
};
首先說一下雙緩沖。我繼承於CDoubleBufferWindowImpl。在消息映射中,我們要讓我們不處理的消息交給基類處理
[cpp]
typedef CWTLTitleWindow _thisClass;
typedef CDoubleBufferImpl<_thisClass> _baseDblBufImpl;
[cpp] view plaincopy
CHAIN_MSG_MAP(_baseDblBufImpl)
同時實現DoPaint函數。
[cpp]
void CWTLTitleWindow::DoPaint( HDC dc )
{
CRect rc;
GetClientRect(&rc);
CMemoryDC MemDc( dc, rc );
HBRUSH hBitmapBrush = CreatePatternBrush(m_hBitmap);
if ( NULL == hBitmapBrush ) {
return;
}
MemDc.FillRect( &rc, hBitmapBrush );
DeleteObject( hBitmapBrush );
}
m_bBitmap是我在資源文件中的一個bmp圖片,我們在Start函數中將其載入。在類釋放時,將其delete。
[cpp]
CWTLTitleWindow::~CWTLTitleWindow(void)
{
if ( NULL != m_hBitmap ) {
DeleteObject( m_hBitmap );
}
}
VOID CWTLTitleWindow::Start()
{
#pragma warning(push)
#pragma warning(disable:4312)
if ( NULL == m_hInstance ) {
m_hInstance = (HINSTANCE)GetWindowLong( GWL_HINSTANCE );
}
#pragma warning(pop)
if ( NULL == m_hBitmap ) {
m_hBitmap = LoadBitmap( m_hInstance, MAKEINTRESOURCE(IDB_BITMAP1));
}
}
以上基本上算是完成了雙緩沖的操作了,但是為了盡量減少刷新的次數,我會多加個判斷:改變的位置和大小是否和現在的位置和大小一致,如果一致則不做任何操作,否則刷新。
[cpp]
ECalcResult CWTLTitleWindow::CalcTitleWindowXY(int& x, int& y )
{
ECalcResult eResult = EError;
do {
RECT rcAttachWindow;
if ( FALSE == ::GetWindowRect( m_hAttachHWnd, &rcAttachWindow ) ) {
break;
}
if ( rcAttachWindow.left == m_hAttachWindowRect.left
&& rcAttachWindow.right == m_hAttachWindowRect.right
&& rcAttachWindow.top == m_hAttachWindowRect.top
&& rcAttachWindow.bottom == m_hAttachWindowRect.bottom )
{
eResult = ENoChange;
break;
}
else {
m_hAttachWindowRect = rcAttachWindow;
}
x = ( rcAttachWindow.left + rcAttachWindow.right - TIPWINDTH ) / 2;
y = rcAttachWindow.top;
eResult = ESuc;
} while (0);
return eResult;
}
再說下無焦點窗口的細節。
首先窗口樣式要有WS_POPUP,網上有人說還要加上WS_VISIBLE,但是我覺得沒必要。其次擴展屬性要有WS_EX_NOACTIVATE。再次我們要處理WM_MOUSEACTIVATE消息。
[cpp]
MESSAGE_HANDLER( WM_MOUSEACTIVATE, OnMouseActive )
[cpp] view plaincopy
LRESULT CWTLTitleWindow::OnMouseActive( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{
return MA_NOACTIVATE; // MA_NOACTIVATEANDEAT亦可
}
最後要特別注意下窗口顯示和移動對焦點的影響。在窗口顯示時,如果我們使用ShowWindow和MoveWindow這類的函數,會導致我們我們窗口還可以獲得焦點。我們要使用SetWindowPos,最後一個參數要帶上SWP_NOACTIVATE。
[cpp]
VOID CWTLTitleWindow::ShowWindow()
{
if ( FALSE == IsBaseWindow( m_hAttachHWnd ) ) {
return;
}
int x = 0;
int y = 0;
ECalcResult eResult = CalcTitleWindowXY( x, y );
if ( EError == eResult ) {
::SetWindowPos( m_hWnd, NULL, x, y, TIPWINDTH, TIPHEIGHT, SWP_NOACTIVATE | SWP_HIDEWINDOW );
return;
}
else if ( ENoChange == eResult ) {
return;
}
::SetWindowPos( m_hWnd, NULL, x, y, TIPWINDTH, TIPHEIGHT, SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
最後說一下業務相關的消息傳遞。在被注入進程的頂層窗口接受到一些消息後,我們會將這些消息傳遞給我們的窗口,讓其做一些處理。為了區分消息來源於頂層窗口還是自己,我將頂層窗口消息處理為一個用戶自定義消息。
[cpp]
VOID CWTLTitleWindow::DealMsg( UINT uMsg )
{
::PostMessage( m_hWnd, WM_USER + uMsg, NULL, NULL );
}
消息映射是這麼寫的,用於處理整個用戶自定義消息(而不會處理頂層窗口傳來的其用戶自定義消息)
[cpp]
MESSAGE_RANGE_HANDLER( WM_USER, WM_USER + WM_USER, OnDealUserMsg )
[cpp] view plaincopy
LRESULT CWTLTitleWindow::OnDealUserMsg( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{
UINT uAttachedWindowMsg = uMsg - WM_USER;
if ( IsNeedShowMsg(uAttachedWindowMsg) ) {
ShowWindow();
}
else {
::PostMessage( m_hWnd, uAttachedWindowMsg, wParam, lParam );
}
return 1;
}
外框窗口和標題窗口基本類似,但是其背景是使用畫筆畫的,而不是通過貼圖。另一個很大的區別就是外框窗口是一個空心的異形窗口。這些區別的主要體現是在DoPaint函數中
[cpp]
void CWTLOutSideWindow::DoPaint( HDC dc )
{
CRect rc; www.2cto.com
GetClientRect(&rc);
CMemoryDC MemDc( dc, rc );
HRGN RgnInside = CreateRectRgn( rc.left + WIDTHHEIGHTADD, rc.top + WIDTHHEIGHTADD,
rc.right - WIDTHHEIGHTADD, rc.bottom - WIDTHHEIGHTADD );
HRGN RgnOut = CreateRectRgn( rc.left, rc.top, rc.right, rc.bottom );
CombineRgn( RgnOut, RgnOut, RgnInside, RGN_DIFF );
MemDc.FillRgn( RgnOut, m_brush );
SetWindowRgn( RgnOut, TRUE ); // 設置異形窗口
DeleteObject( RgnInside );
DeleteObject( RgnOut );
}