⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 在nt系列操作系统里让自己“消失”.txt

📁 ROOTKIT 相关
💻 TXT
📖 第 1 页 / 共 4 页
字号:
    KeyHandle是高位的键的句柄,我们使用NtEnumerateKey的这个值。DesaireAccess是访问权力。KEY_ENUMERATE_SUB_KEYS是它的正确的值。ObjectAttributes描述了我们要打开的子键(包括了它的名字)。

    #define KEY_ENUMERATE_SUB_KEYS 8

    如果NtOpenKey返回0表示打开成功,意味着这个来自我们列表中的键是存在的。被打开的键通过NtClose来关闭。

    NTSTATUS NtClose(
        IN HANDLE Handle
    );

    
    对每次NtEnumareteKey的调用我们要计算的改变,数量上等同于我们列表中存在于注册表指定部分的键的数量。然后我们把改变的数量加到变量Index,最后调用原始的NtEnumerateKey。
    我们使用KeyInformationClass的KeyBasicInformation来获得用索引标明的键的名字。    

    #define KeyBasicInformation 0

    NtEnumerateKey在KeyInformation缓冲区中返回这个结构:

    typedef struct _KEY_BASIC_INFORMATION {
        LARGE_INTEGER LastWriteTime;
        ULONG TitleIndex;
        ULONG NameLength;
        WCHAR Name[1];            
    } KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;

    这里我们只需要的东西是Name和它的长度NameLength。    
    如果没有被转移的索引的记载我们就返回错误STATUS_EA_LIST_INCONSISTENT。

    #define STATUS_EA_LIST_INCONSISTENT 0x80000014


=====[ 5.2 NtEnumerateValueKey ]============================

    注册表键值不是按字母分类的。幸运的是在一个键里键值的数目比较少,所以我们可以通过重调的方法来获得改变的数目。用来获取一个键值信息的API是NtEnumerateValueKey。

    NTSTATUS NtEnumerateValueKey(
        IN HANDLE KeyHandle,
        IN ULONG Index,
        IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
        OUT PVOID KeyValueInformation,
        IN ULONG KeyValueInformationLength,
        OUT PULONG ResultLength
    );

    KeyHandle也是等级高的键的句柄。Index是所给键中键值的索引。KeyValueInformationClass描述信息的类型,保存在KeyValueInformation缓冲区中,缓冲区以字节为大小为KeyValueInformationLength。写入字节的数量返回在ResultLength中。
    我们通过用0到Index的所有索引重调函数计算转移。键值的名字通过把KeyValueInformationClass设置为KeyValueBasicInformation来获取。
    
    #define KeyValueBasicInformation 0


    然后我们获取在KeyValueInformation缓冲区中接下来的数据结构:

    typedef struct _KEY_VALUE_BASIC_INFORMATION {
        ULONG TitleIndex;
        ULONG Type;
        ULONG NameLength;
        WCHAR Name[1];
    } KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION;

    这里我们只对Name和NameLength感兴趣。

    
    如果这里没有被转移的索引记载我们就返回错误STATUS_NO_MORE_ENTRIES。

    #define STATUS_NO_MORE_ENTRIES 0x8000001A



=====[ 6. 系统服务和驱动 ]====================================

    系统服务和驱动是通过4个独立的API函数枚举的。它们在每个Windows版本中的联系都不一样。所以我们必须挂钩所有4个函数。

    BOOL EnumServicesStatusA(
        SC_HANDLE hSCManager,
        DWORD dwServiceType,
        DWORD dwServiceState,
        LPENUM_SERVICE_STATUS lpServices,
        DWORD cbBufSize,
        LPDWORD pcbBytesNeeded,
        LPDWORD lpServicesReturned,
        LPDWORD lpResumeHandle
    );

    BOOL EnumServiceGroupW(
        SC_HANDLE hSCManager,
        DWORD dwServiceType,
        DWORD dwServiceState,
        LPBYTE lpServices,
        DWORD cbBufSize,
        LPDWORD pcbBytesNeeded,
        LPDWORD lpServicesReturned,
        LPDWORD lpResumeHandle,
        DWORD dwUnknown
    );

    BOOL EnumServicesStatusExA(
        SC_HANDLE hSCManager,
        SC_ENUM_TYPE InfoLevel,
        DWORD dwServiceType,
        DWORD dwServiceState,
        LPBYTE lpServices,
        DWORD cbBufSize,
        LPDWORD pcbBytesNeeded,
        LPDWORD lpServicesReturned,
        LPDWORD lpResumeHandle,
        LPCTSTR pszGroupName
    );

    BOOL EnumServicesStatusExW(
        SC_HANDLE hSCManager,
        SC_ENUM_TYPE InfoLevel,
        DWORD dwServiceType,
        DWORD dwServiceState,
        LPBYTE lpServices,
        DWORD cbBufSize,
        LPDWORD pcbBytesNeeded,
        LPDWORD lpServicesReturned,
        LPDWORD lpResumeHandle,
        LPCTSTR pszGroupName
    );


    这里最重要的是lpService,它指向保存服务列表的缓冲区。而指向结果中记录个数的lpServicesReturned也很重要。输出缓冲区中的数据结构取决于函数类型。函数EnumServicesStatusA和
EnumServicesGroupW返回这个结构:

    typedef struct _ENUM_SERVICE_STATUS {
        LPTSTR lpServiceName;
        LPTSTR lpDisplayName;
        SERVICE_STATUS ServiceStatus;
    } ENUM_SERVICE_STATUS, *LPENUM_SERVICE_STATUS;

    typedef struct _SERVICE_STATUS {
        DWORD dwServiceType;
        DWORD dwCurrentState;
        DWORD dwControlsAccepted;
        DWORD dwWin32ExitCode;
        DWORD dwServiceSpecificExitCode;
        DWORD dwCheckPoint;
        DWORD dwWaitHint;
    } SERVICE_STATUS, *LPSERVICE_STATUS;

函数EnumServicesStatusExA和EnumServicesStatusExW返回这个:

    typedef struct _ENUM_SERVICE_STATUS_PROCESS {
        LPTSTR lpServiceName;
        LPTSTR lpDisplayName;
        SERVICE_STATUS_PROCESS ServiceStatusProcess;
    } ENUM_SERVICE_STATUS_PROCESS, *LPENUM_SERVICE_STATUS_PROCESS;

    typedef struct _SERVICE_STATUS_PROCESS {
        DWORD dwServiceType;
        DWORD dwCurrentState;
        DWORD dwControlsAccepted;
        DWORD dwWin32ExitCode;
        DWORD dwServiceSpecificExitCode;
        DWORD dwCheckPoint;
        DWORD dwWaitHint;
        DWORD dwProcessId;
        DWORD dwServiceFlags;
    } SERVICE_STATUS_PROCESS, *LPSERVICE_STATUS_PROCESS;


    我们只对lpServiceName感兴趣因为它是系统服务的名字。所有记录都有静态的大小,所以我们想要隐藏一个的话就需要将之后所有记录向前移它的大小。这里我们必须区分SERVICE_STATUS和SERVICE_STATUS_PROCESS的大小。



=====[ 7. 动态挂钩和扩展 ]=====================================

    为达到预想的效果我们需要挂钩所有正在运行的进程和所有将要被创建的进程。所有新进程都必须在它们运行第一条指令前被挂钩,否则它们就能够在被挂够前看到被隐藏的对象。
    

=====[ 7.1 权限 ]=============================================

    首先我们得知道我们至少获得管理员administrator权限来获得进入所有正在运行的进程。最好的可能是将我们的进程当做系统服务来运行,因为它运行与SYSTEM用户权限下。为安装服务我们首先得获取特殊的权限。
    获取SeDebugPrivilege的权限是很有用的,通过调用OpenProcessToken、LookupPrivilegeValue
和AdjustTokenPrivileges来完成。

    BOOL OpenProcessToken(
        HANDLE ProcessHandle,
        DWORD DesiredAccess,
        PHANDLE TokenHandle
    );

    BOOL LookupPrivilegeValue(
        LPCTSTR lpSystemName,
        LPCTSTR lpName,
        PLUID lpLuid
    );

    BOOL AdjustTokenPrivileges(
        HANDLE TokenHandle,
        BOOL DisableAllPrivileges,
        PTOKEN_PRIVILEGES NewState,
        DWORD BufferLength,
        PTOKEN_PRIVILEGES PreviousState,
        PDWORD ReturnLength
    );


    代码如下:

    #define SE_PRIVILEGE_ENABLED    0x0002
    #define TOKEN_QUERY        0x0008
    #define TOKEN_ADJUST_PRIVILEGES    0x0020

    HANDLE hToken;
    LUID DebugNameValue;
    TOKEN_PRIVILEGES Privileges;
    DWORD dwRet;

    OpenProcessToken(GetCurrentProcess(),
             TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,hToken);
    LookupPrivilegeValue(NULL,"SeDebugPrivilege",&DebugNameValue);
    Privileges.PrivilegeCount=1;
    Privileges.Privileges[0].Luid=DebugNameValue;
    Privileges.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
    AdjustTokenPrivileges(hToken,FALSE,&Privileges,sizeof(Privileges),
                  NULL,&dwRet);
    CloseHandle(hToken);


=====[ 7.2 全局挂钩 ]=======================================

    枚举进程通过前面提到的API函数NtQuerySystemInformation来完成。因为系统中还有一些内部native进程,所以使用重写函数第一个指令的方法来挂钩。对每个正在运行的进程我们需要做的都一样。首先在目标进程里分配一部分内存用来写入我们用来挂钩函数的新代码,然后把每个函数开始的5个字节改为跳转指令(jmp),这个跳转会转为执行我们的代码。所以当被挂钩的函数被调用时跳转指令能立刻被执行。我们需要保存每个函数开始被改写的指令,需要它们来调用被挂钩函数的原始代码。保存指令的过程在"挂钩Windows API"的3.2.3节有描述。
    首先通过NtOpenProcess打开目标进程并获取句柄。如果我们没有足够权限的话就会失败。

    NTSTATUS NtOpenProcess(
        OUT PHANDLE ProcessHandle,
        IN ACCESS_MASK DesiredAccess,
        IN POBJECT_ATTRIBUTES ObjectAttributes,
        IN PCLIENT_ID ClientId OPTIONAL
    );

    ProcessHandle是指向保存进程对象句柄的指针。DesiredAccess应该被设置为PROCESS_ALL_ACCESS。我们要在ClientId结构里设置UniqueProcess为目标进程的PID,UniqueThread应该为0。被打开的句柄可以通过NtClose关闭。

    #define PROCESS_ALL_ACCESS 0x001F0FFF

    现在我们为我们的代码分配部分内存。这通过NtAllocateVirtualMemory来完成。

    NTSTATUS NtAllocateVirtualMemory(
        IN HANDLE ProcessHandle,
        IN OUT PVOID BaseAddress,
        IN ULONG ZeroBits,
        IN OUT PULONG AllocationSize,
        IN ULONG AllocationType,
        IN ULONG Protect
    );

    ProcessHandle是来自NtOpenProcess相同参数。BaseAddress是一个指针,指向被分配虚拟内存基地址的开始处,它的输入参数应该为NULL。AllocationSize指向我们要分配的字节数的变量,同样它也用来接受实际分配的字节数大小。最好把AllocationType在设置成MEM_COMMIT之外再加上MEM_TOP_DOWN因为内存要在接近DLL地址的尽可能高的地址分配。

    #define MEM_COMMIT    0x00001000
    #define MEM_TOP_DOWN    0x00100000    


    然后我们就可以通过调用NtWriteVirtualMemory来写入我们的代码。

    NTSTATUS NtWriteVirtualMemory(
        IN HANDLE ProcessHandle,
        IN PVOID BaseAddress,
        IN PVOID Buffer,
        IN ULONG BufferLength,
        OUT PULONG ReturnLength OPTIONAL
    );

    BaseAddress是NtAllocateVirtualMemory返回的地址。Buffer指向我们要写入的字节,BufferLength是我们要写入的字节数。

    现在我们来挂钩单个进程。被加载入所有进程的动态链接库只有ntdll.dll。所以我们要检查被导入进程要挂钩的函数是否来自ntdll.dll。但是这些来自其它DLL的函数所在的内存可能已经被分配,这时重写它的代码会在目标进程里导致错误。这就是我们必须去检查我们要挂钩的函数来自的动态链接库是否被目标进程加载的原因。
    我们需要通过NtQueryInformationProcess获取目标进程的PEB(进程环境块)。

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -