像一些0day防御系统,当我们打出来whoami等等极具特征
的命令时,会有一个基于主机侧命令执行检测
,会出现告警
,那我们就需要进行一些规避
,用来像权限查询,判断主站上有什么东西,我们该如何进行信息收集
。
场景为当我们的C2上线后,或WebShell直接上线之后,我们可以用一些Windows的api进行信息收集
GetUserNameW:检索与当前线程关联的用户的名称
#include <Windows.h>
#include <stdio.h>
#include <Psapi.h>
#include <LM.h> int main{
WCHAR userName[UNLEN +1];// 定义了一个宽字符数组用于存储用户名
DWORD userNameLength = UNLEN +1;// UNLEN是一个预定义的宏,表示Windows用户名的最大长度,加1是为了存储字符串的终止符
if(GetUserNameW(userName,&userNameLength)){
// userNane是用来接收用户名的缓冲区指针
// &userNameLength,是指向DWORD类型的指针,表示缓冲区的大小,并在成功后返回实际的用户名长度
wprintf(L"User: %ls\\n", userName);// 如果为True则会获取当前用户的用户名,并输出
}
else{
wprintf(L"[-] E: %d",GetLastError());// 如果为False则会打印错误信息
}
}
这里我们用到了GetUserNameW
这个Windows api(GetUserNameW是GetUserName的宽字符版本),宽字符版本使用 UTF-16 编码,适用于处理 Unicode 字符串。
这是函数定义
BOOL GetUserNameW(
LPWSTR lpBuffer,
LPDWORD pcbBuffer
);
lpBuffer
是一个LPWSTR
类型的指向一个缓冲区的指针,用于接收当前用户的登录名。缓冲区的大小由pcbBuffer
指定。
pcbBuffe
r是一个LPDWORD
类型指向一个变量,该变量指定lpBuffer
缓冲区的大小(以字符为单位)。
在函数返回时,该变量包含复制到缓冲区中的字符数,包括终止的空字符。如果缓冲区大小不够,函数会失败,并在此变量中返回所需的缓冲区大小。
True,lpBuffer包含当前用户的登录名
False,GetLastError函数获取更多的错误信息
这里我们可以深入,会涉及两个东西,关于beacon:
一个是cs-bof加载
一个是inline-execute
像我们一些高强的对抗的环境下,后续的信息收集一定是基于这种的情况下去进行,而不是打开cmd执行whoami这种
GetComputerNameExW:检索与本地计算机关联的 NetBIOS 或 DNS 名称。这些名称是在系统启动时建立的,即系统从注册表中读取它们
BOOL GetComputerNameExW(
COMPUTER_NAME_FORMAT NameType,
LPWSTR lpBuffer,
LPDWORD nSize
);
NameType
: 指定要获取的计算机名的格式,这个参数是一个COMPUTER_NAME_FORMAT枚举值 常用的枚举值包括:
• ComputerNameNetBIOS: 获取 NetBIOS 名称。
• ComputerNameDnsHostname: 获取 DNS 主机名。
• ComputerNameDnsDomain: 获取 DNS 域名。
• ComputerNameDnsFullyQualified: 获取 DNS 完全限定域名 (FQDN)。
• ComputerNamePhysicalNetBIOS: 获取物理 NetBIOS 名称。
• ComputerNamePhysicalDnsHostname: 获取物理 DNS 主机名。
• ComputerNamePhysicalDnsDomain: 获取物理 DNS 域名。
• ComputerNamePhysicalDnsFullyQualified: 获取物理 DNS 完全限定域名 (FQDN)。
lpBuffer
: 指向一个缓冲区,用于接收计算机名称。
nSize
: 指向缓冲区大小的指针。输入时为缓冲区的大小,以字符为单位;输出时为存储在缓冲区中的计算机名的字符数,包括终止的 null 字符。
函数执行成功,返回 TRUE。
如果失败,返回 FALSE,可以使用 GetLastError 获取详细的错误信息
WCHAR hostName[UNLEN +1];
// 定义一个 WCHAR 数组 hostName 用于存储计算机的完全限定域名(FQDN)。
// UNLEN 是用户名的最大长度常量(256 字符),加上 1 是为了容纳终止的 null 字符。DWORD hostNameLength = UNLEN +1;
// 定义一个 DWORD 类型的变量 hostNameLength,并将其初始化为 UNLEN + 1。
// 这个变量表示 hostName 数组的大小,以字符为单位。
if(GetComputerNameExW(ComputerNameDnsFullyQualified, hostName,&hostNameLength)){
wprintf(L"Hostname: %ls\n", hostName);
}
// 调用 GetComputerNameExW 函数获取计算机的完全限定域名(FQDN)。
// 如果调用成功,输出计算机的主机名。
else{
wprintf(L"[-] E: %d",GetLastError());
}
// 如果 GetComputerNameExW 调用失败,输出错误代码。
WCHAR dnshostName[UNLEN +1];
// 定义一个 WCHAR 数组 dnshostName 用于存储计算机的 DNS 域名。
DWORD DNShostNameLength= UNLEN +1;
// 定义一个 DWORD 类型的变量 DNShostNameLength,并将其初始化为 UNLEN + 1。
// 这个变量表示 dnshostName 数组的大小,以字符为单位。
if(GetComputerNameExW(ComputerNameDnsDomain, dnshostName,&DNShostNameLength)){
wprintf(L"ComputerNameDnsDomain: %ls\n", dnshostName);
}
// 调用 GetComputerNameExW 函数获取计算机的 DNS 域名。
// 如果调用成功,输出 DNS 域名。
else{
wprintf(L"[-] E: %d",GetLastError());
}
// 如果 GetComputerNameExW 调用失败,输出错误代码。
WCHAR CdnshostName[UNLEN +1];
// 定义一个 WCHAR 数组 CdnshostName 用于存储计算机的物理 DNS 域名。
DWORD CDNShostNameLength= UNLEN +1;
// 定义一个 DWORD 类型的变量 CDNShostNameLength,并将其初始化为 UNLEN + 1。
// 这个变量表示 CdnshostName 数组的大小,以字符为单位。
if(GetComputerNameExW(ComputerNamePhysicalDnsDomain,CdnshostName,&CDNShostNameLength)){
wprintf(L"ComputerNamePhysicalDnsDomain: %ls\n",CdnshostName);
}
// 调用 GetComputerNameExW 函数获取计算机的物理 DNS 域名。
// 如果调用成功,输出物理 DNS 域名。
else{
wprintf(L"[-] E: %d",GetLastError());
}
// 如果 GetComputerNameExW 调用失败,输出错误代码。
RtlGetVersion:返回有关当前正在运行的操作系统的版本信息
NTSTATUS RtlGetVersion(
PRTL_OSVERSIONINFOW lpVersionInformation
);
lpVersionInformation
: 指向 RTL_OSVERSIONINFOW 结构的指针,该结构用于接收操作系统的版本信息
返回一个 NTSTATUS 类型的值,表示函数调用的结果。如果函数成功,返回的状态值通常为 STATUS_SUCCESS(即 0)。
typedef struct _OSVERSIONINFOW {
ULONG dwOSVersionInfoSize; // 结构的大小(以字节为单位)
ULONG dwMajorVersion; // 操作系统的主版本号
ULONG dwMinorVersion; // 操作系统的次版本号
ULONG dwBuildNumber; // 操作系统的构建编号
ULONG dwPlatformId; // 操作系统平台标识
WCHAR szCSDVersion[128]; // 服务包信息
} RTL_OSVERSIONINFOW, *PRTL_OSVERSIONINFOW;
#include <Windows.h>
#include <stdio.h>
#include <Psapi.h>
#include <LM.h>// 定义函数指针类型
typedef NTSYSAPI NTSTATUS(WINAPI* procRtlGetVersion)(PRTL_OSVERSIONINFOW lpVersionInformation);
// 是将STATUS_SUCCESS作为0x00000000的一个替代符号,这样你可以在代码中使用STATUS_SUCCESS来表示操作成功
#define STATUS_SUCCESS 0x00000000
intmain(){
// 初始化一个 RTL_OSVERSIONINFOW 结构体,并将其所有成员设置为零
RTL_OSVERSIONINFOW osversioninfow ={0};
// 获取 ntdll.dll 模块的句柄,该模块包含了 Windows 的低级函数
HMODULE hntdll =GetModuleHandleA("ntdll.dll");
// 从 ntdll.dll 模块中获取 RtlGetVersion 函数的地址
procRtlGetVersion myRtlGetVersion =(procRtlGetVersion)GetProcAddress(hntdll,"RtlGetVersion");
// 如果成功检索并调用了 RtlGetVersion 函数
if(myRtlGetVersion(&osversioninfow)== STATUS_SUCCESS){
// 打印 Windows 操作系统的主版本号和次版本号
wprintf(L"Windows Version: %lu.%lu\\n", osversioninfow.dwMajorVersion, osversioninfow.dwMinorVersion);
// 打印 Windows 操作系统的内部版本号
wprintf(L"Windows Build: %lu\\n", osversioninfow.dwBuildNumber);
}
}
像我们上升到xdr的对抗,走beacon内存加载,在beacon中就要完成脱钩
的操作,如果没有做脱钩的话,我们就到更深一层的维度,更底层的调用。
virtualprotect
为主,首先调用user32dll里面 转到kernel32dll 转到ntdll 最后通过syscall在内核中完成修改一块内存的保护属性。
GetCurrentDirectory :检索当前进程的当前目录
DWORD GetCurrentDirectory(
DWORD nBufferLength, // 指定缓冲区的大小,单位为字符
LPTSTR lpBuffer // 指向存储当前目录路径的缓冲区
);
nBufferLength:
指定缓冲区 lpBuffer 的大小,单位是字符数。这个大小应该足够大以容纳当前目录的路径,包括终止符 '\0'。
lpBuffer:
指向一个字符数组(缓冲区),用于接收当前目录的绝对路径。
如果函数成功,返回值是存储在缓冲区中的字符串的长度。
如果缓冲区大小不足以存储路径,函数将返回所需的缓冲区大小(包括终止符),并且不会将路径存入lpBuffer中。
如果函数调用失败,返回值为0。在这种情况下,可以调用 GetLastError 函数来获取详细的错误信息。
WCHAR currentDir[MAX_PATH +1];
// 定义一个 WCHAR 数组 currentDir 用于存储当前目录路径。
// MAX_PATH 是 Windows 系统中定义的最大路径长度(通常为 260 字符),加上 1 是为了容纳结尾的 null 字符。DWORD currentDirLength = MAX_PATH +1;
// 定义一个 DWORD 类型的变量 currentDirLength,并将其初始化为 MAX_PATH + 1。
// 这个变量表示 currentDir 数组的大小,以字符为单位。
if(GetCurrentDirectoryW(currentDirLength, currentDir)!=0){
wprintf(L"Pwd: %ls\\n", currentDir);
}
// 调用 GetCurrentDirectoryW 函数获取当前工作目录。
// 如果调用成功,输出当前目录路径。
else{
wprintf(L"[-] E: %d",GetLastError());
}
// 如果 GetCurrentDirectoryW 调用失败,输出错误代码。
GetTickCount64:检索自系统启动以来经过的毫秒数
来判断他到底会不会经常关机,如果动静不大,webshell独家 那就不需要维权
白加黑+测加载 白是在计划任务(很少的情况) --> 成本性问题 edr或xdr
常规的exe不要在有edr或xdr的服务器中进行启动
常规的exe进行维权 加启动项 计划任务 com劫持,但是我们不会有签名
侧重到webshell 隐匿性 打不打内存马
ULONGLONG GetTickCount64(void);
这个函数不接受任何参数
返回一个 ULONGLONG 类型的值,该值表示自系统启动以来经过的时间(以毫秒为单位)
DWORD tickCount = (DWORD)GetTickCount64();
// 调用 GetTickCount64 函数获取系统启动后的运行时间(以毫秒为单位),并将其转换为 DWORD 类型。
// 注意:这里将 64 位的时间值强制转换为 32 位的 DWORD 类型,这在系统运行时间超过大约 49.7 天(即 32 位的最大值 4294967295 毫秒)后可能导致数据丢失。DWORD dcuptime = ((tickCount / 1000) / 60);
// 将获取到的毫秒数除以 1000 转换为秒,再除以 60 转换为分钟。
// 结果是系统启动后的运行时间,以分钟为单位。
wprintf(L"Uptime: %lu minutes\\n", dcuptime);
// 使用 wprintf 函数输出系统启动后的运行时间,格式化为以分钟为单位的无符号长整型 (DWORD) 值。
// "%lu" 是用于无符号长整型的格式说明符。
获取系统运行时间
DWORD systemUptime = GetTickCount64();
GetTickCount64 函数获取从系统启动以来的毫秒数。systemUptime 存储这个值,用来表示系统的总运行时间。
初始化变量
DWORD idleTicks = 0,
idletime = 0,
lastInputTicks = 0;
这里定义了三个 DWORD 类型的变量:
• idleTicks 用于存储系统空闲时间的毫秒数。
• idletime 用于存储计算后的空闲时间,以分钟为单位。
• lastInputTicks 用于存储最后一次输入操作的时间(毫秒)。
• 设置 LASTINPUTINFO 结构体
LASTINPUTINFO lastInputInfo;
lastInputInfo.cbSize = sizeof(lastInputInfo);
lastInputInfo.dwTime = 0;
LASTINPUTINFO 结构体用于存储最后一次输入信息
cbSize 成员设置为结构体的大小,以确保函数 GetLastInputInfo 正确读取数据
dwTime 初始化为0
获取最后一次输入信息
if (GetLastInputInfo(&lastInputInfo)) {
lastInputTicks = lastInputInfo.dwTime;
idleTicks = systemUptime - lastInputTicks;
}
GetLastInputInfo 函数填充 lastInputInfo 结构体。如果函数成功返回(即 if 语句中的条件为真),将 lastInputTicks 设置为最后一次输入的时间(从系统启动到最后一次输入的毫秒数)。然后计算空闲时间 idleTicks,即当前系统时间减去最后一次输入时间。
计算空闲时间(以分钟为单位)
idletime = ((idleTicks > 0) ? ((idleTicks / 1000) / 60) : 0);
这行代码首先检查 idleTicks 是否大于0。如果是,则将 idleTicks 从毫秒转换为秒(idleTicks / 1000),然后再转换为分钟(除以60)。如果 idleTicks 小于等于0,则将 idletime 设置为0。
输出空闲时间
wprintf(L"Idletime: %lu minutes\\n", idletime);
完整代码
#include <Windows.h>
#include <stdio.h>
#include <Psapi.h>
#include <LM.h>
typedef NTSYSAPI NTSTATUS(WINAPI* procRtlGetVersion)(PRTL_OSVERSIONINFOW lpVersionInformation);
#define STATUS_SUCCESS 0x00000000intmain(){
DWORD tickCount =(DWORD)GetTickCount64();
DWORD dcuptime =((tickCount /1000)/60);
wprintf(L"Uptime: %lu minutes\n", dcuptime);
DWORD systemUptime =GetTickCount64();
DWORD idleTicks =0, idletime =0, lastInputTicks =0;
LASTINPUTINFO lastInputInfo;
lastInputInfo.cbSize =sizeof(lastInputInfo);
lastInputInfo.dwTime =0;
if(GetLastInputInfo(&lastInputInfo)){
lastInputTicks = lastInputInfo.dwTime;
idleTicks = systemUptime - lastInputTicks;
}
idletime =((idleTicks >0)?((idleTicks /1000)/60):0);
wprintf(L"Idletime: %lu minutes\n", idletime);
}
我们为什么要看服务,是因为像CrowdStrike,每家公司买了之后进程名是不固定的,可以定制化,我们从进程名中不一定能看出来他是一个杀软,所以我们要看服务,有没有什么一些比较明显的服务名像monitor等等,或者复制出来到杀软识别中进行识别
打开服务控制管理器
SC_HANDLE hRemoteSvcManager = OpenSCManagerA(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE);
• OpenSCManagerA 函数尝试以枚举服务的权限打开服务控制管理器。第一个NULL 表示操作本地计算机,第二个NULL 表示要打开的服务控制管理器数据库的名称
• 如果函数返回 NULL,说明打开服务控制管理器失败,进入错误处理部分。
• 检查打开服务控制管理器是否成功
if (hRemoteSvcManager == NULL) {
wprintf(L"[-] E: %d", GetLastError());
}
• 如果 hRemoteSvcManager 为 NULL,调用 GetLastError 获取并输出错误码。
• 获取所需的缓冲区大小
DWORD BytesNeeded;
DWORD NumberOfServices;
DWORD ResumeHandleSize=0;
DWORD ServiceStructSize;
LPENUM_SERVICE_STATUSW lpServiceEnumStruct;INT errVal =EnumServicesStatusW(hRemoteSvcManager, SERVICE_WIN32, SERVICE_STATE_ALL,NULL,0,&BytesNeeded,&NumberOfServices,&ResumeHandleSize);
if((errVal == ERROR_INVALID_HANDLE)||(errVal == ERROR_ACCESS_DENIED)||(errVal == ERROR_INVALID_PARAMETER)){
wprintf(L"[-] E: %d",GetLastError());
}
EnumServicesStatusW:这是一个 Windows API 函数,用于枚举服务控制管理器数据库中的服务。它将服务的信息存储在一个 ENUM_SERVICE_STATUSW 结构的数组中
BOOL EnumServicesStatusW(
SC_HANDLE hSCManager,
DWORD dwServiceType,
DWORD dwServiceState,
LPENUM_SERVICE_STATUSW lpServices,
DWORD cbBufSize,
LPDWORD pcbBytesNeeded,
LPDWORD lpServicesReturned,
LPDWORD lpResumeHandle
);
hSCManager
:
类型:SC_HANDLE
说明:服务控制管理器的句柄,可以通过 OpenSCManager 函数获得。这个句柄指定要枚举服务的服务控制管理器。
dwServiceType
:
类型:DWORD
说明:指定要枚举的服务类型。可以是以下值的组合:
• SERVICE_WIN32_OWN_PROCESS: 枚举 Win32 服务,独立于其他服务进程运行。
• SERVICE_WIN32_SHARE_PROCESS: 枚举 Win32 服务,共享进程与其他服务运行。
• SERVICE_DRIVER: 枚举设备驱动程序服务。
• SERVICE_TYPE_ALL: 枚举所有服务。
dwServiceState
:
类型:DWORD
说明:指定要枚举的服务状态。可以是以下值之一:
• SERVICE_ACTIVE: 枚举正在运行的服务。
• SERVICE_INACTIVE: 枚举未运行的服务。
• SERVICE_STATE_ALL: 枚举所有服务,不论它们是否正在运行。
lpServices
:
类型:LPENUM_SERVICE_STATUSW
说明:指向一个缓冲区,该缓冲区接收 ENUM_SERVICE_STATUSW 结构数组,每个结构包含一个服务的名称和状态信息。如果缓冲区不足以存放所有数据,函数将失败,并通过 pcbBytesNeeded 返回所需的缓冲区大小。
cbBufSize
:
类型:DWORD
说明:指定 lpServices 缓冲区的大小(以字节为单位)。
pcbBytesNeeded
:
类型:LPDWORD
说明:指向一个变量,该变量接收 lpServices 所需的字节数。如果缓冲区太小,函数将返回 FALSE,并且该变量包含所需的缓冲区大小。
lpServicesReturned
:
类型:LPDWORD
说明:指向一个变量,该变量接收枚举到的服务数量。
lpResumeHandle
:
类型:LPDWORD
说明:指向一个变量,该变量包含从上次枚举未完成的地方继续的句柄。如果这是第一次调用该函数或不需要恢复句柄,应将 *lpResumeHandle 设为 0。
• hRemoteSvcManager:
• 传入之前通过 OpenSCManagerA 函数获取的服务控制管理器的句柄。
• SERVICE_WIN32:
• 表示枚举的服务类型,这里指定为 SERVICE_WIN32,意味着要枚举的服务包括 Win32 系统服务(但不包括设备驱动程序)
• SERVICE_STATE_ALL:
• 指定要枚举的服务状态,SERVICE_STATE_ALL 表示枚举所有状态的服务(包括运行、暂停、停止的服务)
• NULL 和 0:
• NULL 表示当前未分配缓冲区,0 表示缓冲区大小为 0。这是为了在第一次调用时获得所需的缓冲区大小
• &BytesNeeded:
• 传递一个指针,用于接收函数返回的实际所需缓冲区大小
• &NumberOfServices:
• 传递一个指针,用于接收函数返回的服务数量
• &ResumeHandleSize:
• 传递一个指针,用于接收恢复句柄(如果服务列表太长,需要分段处理)
errVal
• 这是 EnumServicesStatusW 函数的返回值,用于检查调用是否成功。
• 如果返回值是 ERROR_INVALID_HANDLE(表示句柄无效)、ERROR_ACCESS_DENIED(表示访问被拒绝)或 ERROR_INVALID_PARAMETER(表示参数无效),则表示调用失败。
GetLastError()
• 如果函数失败,则调用 GetLastError() 函数获取具体的错误代码,并输出错误信息。
#include <Windows.h>
#include <stdio.h>
#include <Psapi.h>
#include <LM.h>
typedef NTSYSAPI NTSTATUS(WINAPI* procRtlGetVersion)(PRTL_OSVERSIONINFOW lpVersionInformation);
#define STATUS_SUCCESS 0x00000000intmain(){
SC_HANDLE hRemoteSvcManager =OpenSCManagerA(NULL,NULL, SC_MANAGER_ENUMERATE_SERVICE);
if(hRemoteSvcManager ==NULL){
wprintf(L"[-] E: %d",GetLastError());
}
else{
DWORD BytesNeeded;
DWORD NumberOfServices;
DWORD ResumeHandleSize=0;
DWORD ServiceStructSize;
LPENUM_SERVICE_STATUSW lpServiceEnumStruct;
INT errVal =EnumServicesStatusW(hRemoteSvcManager, SERVICE_WIN32, SERVICE_STATE_ALL,NULL,0,&BytesNeeded,&NumberOfServices,&ResumeHandleSize);
if((errVal == ERROR_INVALID_HANDLE)||(errVal == ERROR_ACCESS_DENIED)||(errVal == ERROR_INVALID_PARAMETER)){
wprintf(L"[-] E: %d",GetLastError());
}
else{
ServiceStructSize=BytesNeeded+sizeof(ENUM_SERVICE_STATUSW);
lpServiceEnumStruct =(LPENUM_SERVICE_STATUSW)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,ServiceStructSize*sizeof(WCHAR));
if(lpServiceEnumStruct ==NULL){
wprintf(L"[-] E: %d",GetLastError());
}
else{
errVal =EnumServicesStatusW(hRemoteSvcManager, SERVICE_WIN32, SERVICE_STATE_ALL, lpServiceEnumStruct,ServiceStructSize,&BytesNeeded,&NumberOfServices,&ResumeHandleSize);
if((errVal !=0)){
printf("\n\n[+] Enumerating Services [%lu]\n",NumberOfServices);
for(int i =0; i <(int)NumberOfServices; i++){
SC_HANDLE hSvc =OpenServiceW(hRemoteSvcManager, lpServiceEnumStruct<i>.lpServiceName, SERVICE_QUERY_CONFIG);
if(hSvc !=NULL){
LPQUERY_SERVICE_CONFIGW lpServiceConfig;
DWORD scbytesNeeded;
DWORD ScdwBytes;
QueryServiceConfigW(hSvc,NULL,0,&scbytesNeeded);
ScdwBytes= scbytesNeeded;
lpServiceConfig =(LPQUERY_SERVICE_CONFIGW)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,ScdwBytes*sizeof(WCHAR));
if(lpServiceConfig !=NULL){
errVal =QueryServiceConfigW(hSvc, lpServiceConfig,ScdwBytes,&scbytesNeeded);
if(errVal !=0){
wprintf(L"[+] Display Name: %ls\n", lpServiceEnumStruct<i>.lpDisplayName);
wprintf(L"[.] Service Name: %ls\n", lpServiceEnumStruct<i>.lpServiceName);
wprintf(L"[.] Service User: %ls\n\n", lpServiceConfig->lpServiceStartName);
HeapFree(GetProcessHeap(),0, lpServiceConfig);
CloseServiceHandle(hSvc);
}
else{
wprintf(L"[-] E: %d",GetLastError());
HeapFree(GetProcessHeap(),0, lpServiceConfig);
CloseServiceHandle(hSvc);
}
}
}
else{
wprintf(L"[-] E: %d",GetLastError());
}
}
}
else{
wprintf(L"[-] E: %d",GetLastError());
}
// 尤其在驱动编程中,堆一定要释放,否则就是蓝屏的问题
HeapFree(GetProcessHeap(),0, lpServiceEnumStruct);
}
}
}
}
• 根据之前获取的 BytesNeeded,计算出 ServiceStructSize,并使用 HeapAlloc 为服务状态结构体分配内存。
• 再次调用 EnumServicesStatusW 实际填充服务信息。
• 如果调用成功,遍历服务列表:
• 使用 OpenServiceW 打开每个服务,以获取其配置信息。
• 分配内存并调用 QueryServiceConfigW 获取服务的详细配置信息。
• 输出服务的显示名称、服务名称、当前状态、服务路径、用户和服务类型。
• 释放内存和关闭服务句柄。
• 如果任何步骤失败,输出错误信息。
举一个概念
我们通过web打进去的,webshell入口点,或者反弹shell,或者cs上线,大多数我们都是以土豆系列做提权,因为大多数都是iis帐号,本地命令执行可能已经到system权限
如果是本地钓鱼上来的,个人层用UAC提权是比较多的,我们要考虑的是怎么把他们武器化到我们的cs中来
如果有CrowdStrike的环境,是不建议提权的,除非有很大的的把握,提权的行为不会被捕获到
还需要明确我们提却是为了什么,提权administrator,或者system权限,可能是为了挂驱动,而不是为了去dump hash,也就是对lsass的操作,尤其是在更高强度的对抗环境下,也不要去动lsass,在国内的话基本不用特别注意
国外的xdr,我们可以hook一些键盘记录 --> 令牌窃取 --> net use --> 这样来说和hash PTH 作用是一样的
关闭服务控制管理器句柄
CloseServiceHandle(hRemoteSvcManager);
• 使用 CloseServiceHandle 关闭之前打开的服务控制管理器句柄,释放相关资源
1. 打开进程令牌:获取当前进程的令牌,以便查询其权限。
2. 获取令牌信息:从令牌中获取权限信息。
3. 列出权限:遍历权限列表,并检查每个权限的状态。
#include <windows.h>
#include <stdio.h>voidPrintPrivileges(){
HANDLE hToken;
TOKEN_PRIVILEGES tp;
DWORD dwSiz
PTOKEN_PRIVILEGES pTokenPrivileges;
BOOL result;
// 打开当前进程的令牌
if(!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY,&hToken)){
wprintf(L"[-] Failed to open process token. Error: %d\\n",GetLastError());
return;
}
// 获取令牌信息所需的缓冲区大小
if(!GetTokenInformation(hToken,TokenPrivileges,NULL,0,&dwSize)&&
GetLastError()== ERROR_INSUFFICIENT_BUFFER){
pTokenPrivileges =(PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
if(pTokenPrivileges ==NULL){
wprintf(L"[-] Failed to allocate memory for token information.\\n");
CloseHandle(hToken);
return;
}
// 获取令牌信息
if(GetTokenInformation(hToken,TokenPrivileges, pTokenPrivileges, dwSize,&dwSize)){
wprintf(L"[+] Privileges:\\n");
for(DWORD i =0; i < pTokenPrivileges->PrivilegeCount; i++){
LPWSTR privilegeName =NULL;
DWORD nameSize =0;
WCHAR privilegeNameBuffer[256];
// 获取权限名称
nameSize =sizeof(privilegeNameBuffer)/sizeof(WCHAR);
if(LookupPrivilegeName(NULL,&pTokenPrivileges->Privileges<i>.Luid, privilegeNameBuffer,&nameSize)){
privilegeName = privilegeNameBuffer;
}else{
privilegeName =L"Unknown Privilege";
}
wprintf(L" - %s: %s\\n", privilegeName,(pTokenPrivileges->Privileges<i>.Attributes& SE_PRIVILEGE_ENABLED)?L"Enabled":L"Disabled");
}
}else{
wprintf(L"[-] Failed to get token information. Error: %d\\n",GetLastError());
}
// 释放内存
HeapFree(GetProcessHeap(),0, pTokenPrivileges);
}else{
wprintf(L"[-] Failed to get buffer size for token information. Error: %d\\n",GetLastError());
}
// 关闭令牌句柄
CloseHandle(hToken);
}
intmain(){
PrintPrivileges();
return0;
}
OpenProcessToken用于获取指定进程的访问令牌
BOOL OpenProcessToken(
HANDLE ProcessHandle,
DWORD DesiredAccess,
PHANDLE TokenHandle
);
ProcessHandle
:
类型:HANDLE
说明:指定要打开其访问令牌的进程句柄。可以使用 GetCurrentProcess 函数来获取当前进程的句柄,或使用 OpenProcess 获取其他进程的句柄。
DesiredAccess
:
类型:DWORD
说明:指定所需的访问权限。常用的访问权限包括:
• TOKEN_ADJUST_PRIVILEGES:调整令牌中的权限。
• TOKEN_QUERY:查询令牌的权限。
• TOKEN_DUPLICATE:复制令牌。
• TOKEN_ASSIGN_PRIMARY:将令牌分配为一个进程的主令牌。
• TOKEN_IMPERSONATE:启用模拟权限。
• TOKEN_READ:读取令牌的所有标准访问权限。
• TOKEN_ALL_ACCESS:请求对令牌的完全访问权限。
TokenHandle
:
类型:PHANDLE
说明:指向接收新打开的访问令牌句柄的指针。如果函数成功,TokenHandle 将包含一个有效的句柄,可以用于访问或修改令牌的属性。使用完句柄后,应调用 CloseHandle 释放该句柄。
打开进程令牌
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
wprintf(L"[-] Failed to open process token. Error: %d\\n", GetLastError());
return;
}
• 使用 OpenProcessToken 函数打开当前进程的令牌,并请求 TOKEN_QUERY 权限以便查询令牌信息。
获取令牌信息所需的缓冲区大小
if (!GetTokenInformation(hToken, TokenPrivileges, NULL, 0, &dwSize) &&
GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
...
}
• 使用 GetTokenInformation 函数获取缓冲区大小。第一次调用传入 NULL 来确定需要的缓冲区大小。
分配内存并获取令牌信息
pTokenPrivileges = (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
if(pTokenPrivileges ==NULL){
...
}
if(GetTokenInformation(hToken,TokenPrivileges, pTokenPrivileges, dwSize,&dwSize)){
...
}
• 根据获取的大小分配内存,并再次调用 GetTokenInformation 来实际填充权限信息。
列出权限并输出
for (DWORD i = 0; i < pTokenPrivileges->PrivilegeCount; i++) {
...
wprintf(L" - %s: %s\\n", privilegeName, (pTokenPrivileges->Privileges<i>.Attributes & SE_PRIVILEGE_ENABLED) ? L"Enabled" : L"Disabled");
}
• 遍历所有权限,使用 LookupPrivilegeName 获取每个权限的名称,并检查权限是否启用。
释放内存并关闭句柄
HeapFree(GetProcessHeap(), 0, pTokenPrivileges);
CloseHandle(hToken);
• 释放分配的内存,并关闭令牌句柄以释放系统资源
#include <Windows.h>
#include <stdio.h>
#include <Psapi.h>
#include <LM.h>typedef NTSYSAPI NTSTATUS(WINAPI* procRtlGetVersion)(PRTL_OSVERSIONINFOW lpVersionInformation);
#define STATUS_SUCCESS 0x00000000
voidPrintPrivileges(){
HANDLE hToken;
TOKEN_PRIVILEGES tp;
DWORD dwSize;
PTOKEN_PRIVILEGES pTokenPrivileges;
BOOL result;
// 打开当前进程的令牌
if(!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY,&hToken)){
wprintf(L"[-] Failed to open process token. Error: %d\n",GetLastError());
return;
}
// 获取令牌信息所需的缓冲区大小
if(!GetTokenInformation(hToken,TokenPrivileges,NULL,0,&dwSize)&&
GetLastError()== ERROR_INSUFFICIENT_BUFFER){
pTokenPrivileges =(PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
if(pTokenPrivileges ==NULL){
wprintf(L"[-] Failed to allocate memory for token information.\n");
CloseHandle(hToken);
return;
}
// 获取令牌信息
if(GetTokenInformation(hToken,TokenPrivileges, pTokenPrivileges, dwSize,&dwSize)){
wprintf(L"[+] Privileges:\n");
for(DWORD i =0; i < pTokenPrivileges->PrivilegeCount; i++){
LPWSTR privilegeName =NULL;
DWORD nameSize =0;
WCHAR privilegeNameBuffer[256];
// 获取权限名称
nameSize =sizeof(privilegeNameBuffer)/sizeof(WCHAR);
if(LookupPrivilegeName(NULL,&pTokenPrivileges->Privileges<i>.Luid, privilegeNameBuffer,&nameSize)){
privilegeName = privilegeNameBuffer;
}
else{
privilegeName =L"Unknown Privilege";
}
wprintf(L" - %s: %s\n", privilegeName,(pTokenPrivileges->Privileges<i>.Attributes& SE_PRIVILEGE_ENABLED)?L"Enabled":L"Disabled");
}
}
else{
wprintf(L"[-] Failed to get token information. Error: %d\n",GetLastError());
}
// 释放内存
HeapFree(GetProcessHeap(),0, pTokenPrivileges);
}
else{
wprintf(L"[-] Failed to get buffer size for token information. Error: %d\n",GetLastError());
}
// 关闭令牌句柄
CloseHandle(hToken);
}
intmain(){
WCHAR userName[UNLEN +1];
DWORD userNameLength = UNLEN +1;
if(GetUserNameW(userName,&userNameLength)){
wprintf(L"User: %ls\n", userName);
}
else{
wprintf(L"[-] E: %d",GetLastError());
}
WCHAR hostName[UNLEN +1];
DWORD hostNameLength = UNLEN +1;
if(GetComputerNameExW(ComputerNameDnsFullyQualified, hostName,&hostNameLength)){
wprintf(L"Hostname: %ls\n", hostName);
}
else{
wprintf(L"[-] E: %d",GetLastError());
}
WCHAR dnshostName[UNLEN +1];
DWORD DNShostNameLength= UNLEN +1;
if(GetComputerNameExW(ComputerNameDnsDomain, dnshostName,&DNShostNameLength)){
wprintf(L"ComputerNameDnsDomain: %ls\n", dnshostName);
}
else{
wprintf(L"[-] E: %d",GetLastError());
}
WCHAR CdnshostName[UNLEN +1];
DWORD CDNShostNameLength= UNLEN +1;
if(GetComputerNameExW(ComputerNamePhysicalDnsDomain,CdnshostName,&CDNShostNameLength)){
wprintf(L"ComputerNamePhysicalDnsDomain: %ls\n",CdnshostName);
}
else{
wprintf(L"[-] E: %d",GetLastError());
}
RTL_OSVERSIONINFOW osversioninfow ={0};
HMODULE hntdll =GetModuleHandleA("ntdll.dll");
procRtlGetVersion myRtlGetVersion =(procRtlGetVersion)GetProcAddress(hntdll,"RtlGetVersion");
if(myRtlGetVersion(&osversioninfow)== STATUS_SUCCESS){
wprintf(L"Windows Version: %lu.%lu\n", osversioninfow.dwMajorVersion, osversioninfow.dwMinorVersion);
wprintf(L"Windows Build: %lu\n", osversioninfow.dwBuildNumber);
}
WCHAR currentDir[MAX_PATH +1];
DWORD currentDirLength = MAX_PATH +1;
if(GetCurrentDirectoryW(currentDirLength, currentDir)!=0){
wprintf(L"Pwd: %ls\n", currentDir);
}
else{
wprintf(L"[-] E: %d",GetLastError());
}
DWORD tickCount =(DWORD)GetTickCount64();
DWORD dcuptime =((tickCount /1000)/60);
wprintf(L"Uptime: %lu minutes\n", dcuptime);
DWORD systemUptime =GetTickCount64();
DWORD idleTicks =0, idletime =0, lastInputTicks =0;
LASTINPUTINFO lastInputInfo;
lastInputInfo.cbSize =sizeof(lastInputInfo);
lastInputInfo.dwTime =0;
if(GetLastInputInfo(&lastInputInfo)){
lastInputTicks = lastInputInfo.dwTime;
idleTicks = systemUptime - lastInputTicks;
}
idletime =((idleTicks >0)?((idleTicks /1000)/60):0);
wprintf(L"Idletime: %lu minutes\n", idletime);
SC_HANDLE hRemoteSvcManager =OpenSCManagerA(NULL,NULL, SC_MANAGER_ENUMERATE_SERVICE);
if(hRemoteSvcManager ==NULL){
wprintf(L"[-] E: %d",GetLastError());
}
else{
DWORD BytesNeeded;
DWORD NumberOfServices;
DWORD ResumeHandleSize=0;
DWORD ServiceStructSize;
LPENUM_SERVICE_STATUSW lpServiceEnumStruct;
INT errVal =EnumServicesStatusW(hRemoteSvcManager, SERVICE_WIN32, SERVICE_STATE_ALL,NULL,0,&BytesNeeded,&NumberOfServices,&ResumeHandleSize);
if((errVal == ERROR_INVALID_HANDLE)||(errVal == ERROR_ACCESS_DENIED)||(errVal == ERROR_INVALID_PARAMETER)){
wprintf(L"[-] E: %d",GetLastError());
}
else{
ServiceStructSize=BytesNeeded+sizeof(ENUM_SERVICE_STATUSW);
lpServiceEnumStruct =(LPENUM_SERVICE_STATUSW)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,ServiceStructSize*sizeof(WCHAR));
if(lpServiceEnumStruct ==NULL){
wprintf(L"[-] E: %d",GetLastError());
}
else{
errVal =EnumServicesStatusW(hRemoteSvcManager, SERVICE_WIN32, SERVICE_STATE_ALL, lpServiceEnumStruct,ServiceStructSize,&BytesNeeded,&NumberOfServices,&ResumeHandleSize);
if((errVal !=0)){
printf("\n\n[+] Enumerating Services [%lu]\n",NumberOfServices);
for(int i =0; i <(int)NumberOfServices; i++){
SC_HANDLE hSvc =OpenServiceW(hRemoteSvcManager, lpServiceEnumStruct<i>.lpServiceName, SERVICE_QUERY_CONFIG);
if(hSvc !=NULL){
LPQUERY_SERVICE_CONFIGW lpServiceConfig;
DWORD scbytesNeeded;
DWORD ScdwBytes;
QueryServiceConfigW(hSvc,NULL,0,&scbytesNeeded);
ScdwBytes= scbytesNeeded;
lpServiceConfig =(LPQUERY_SERVICE_CONFIGW)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,ScdwBytes*sizeof(WCHAR));
if(lpServiceConfig !=NULL){
errVal =QueryServiceConfigW(hSvc, lpServiceConfig,ScdwBytes,&scbytesNeeded);
if(errVal !=0){
wprintf(L"[+] Display Name: %ls\n", lpServiceEnumStruct<i>.lpDisplayName);
wprintf(L"[.] Service Name: %ls\n", lpServiceEnumStruct<i>.lpServiceName);
wprintf(L"[.] Service User: %ls\n\n", lpServiceConfig->lpServiceStartName);
HeapFree(GetProcessHeap(),0, lpServiceConfig);
CloseServiceHandle(hSvc);
}
else{
wprintf(L"[-] E: %d",GetLastError());
HeapFree(GetProcessHeap(),0, lpServiceConfig);
CloseServiceHandle(hSvc);
}
}
}
else{
wprintf(L"[-] E: %d",GetLastError());
}
}
}
else{
wprintf(L"[-] E: %d",GetLastError());
}
HeapFree(GetProcessHeap(),0, lpServiceEnumStruct);
}
}
}
CloseServiceHandle(hRemoteSvcManager);
PrintPrivileges();
return0;
}
我们在beacon端
加载到beacon的内存中
inline-execute c:\user\27682\Project1.exe
cs-bof加载
inline-execute Project.obj 需要按照cs的标准开发
分叉和运行:fork&run
bof:可以理解为在beacon内部执行的一个插件,用来替代cs的分叉运行
木马:a.exe
我们在beacon端中输入 beacon>shell whoami
调用链如下: a.exe -> cmd.exe -> whoami.exe(传统的分叉运行进程)
我们使用 举例:InlineExecute-Assembly 去执行 GetUserNameW
我们在beacon端 beacon>InlineExecute-Assembly C:\user\27682\Project1.exe
调用链如下: a.exe 内部去执行了Project1.exe,这样就不会有多余的进程,且在进程调用链是看不见Project1.exe的
需要注意的是,因为在beacon内部执行,像万一有一些逻辑方面的问题,卡死在那里,也会导致我们的beacon会话死掉
cs的bof开发很繁琐,很多函数用不了,且需要更改为cs支持的格式,所以更推荐使用inline的方式进行执行,这个GitHub项目也比较老了,可以进行一些修改再继续使用
这个项目中涉及到了AMSI反病毒扫描接口 默认在df环境下 现在的edr/xdr是集成了AMSI的功能
会检测powershell的执行,如果可以进行绕过AMSI就可以任意执行powershell
ETW就是Windows事件记录,也可以对其进行hook
https://www.t00ls.com/articles-72415.html