📄 windows进程中的内存结构.txt
字号:
ULONG PeakHandleCount;
ULONG Reserved2[4];
ULONG InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ULONG ValidAccess;
UCHAR Unknown;
BOOLEAN MaintainHandleDatabase;
POOL_TYPE PoolType;
ULONG PagedPoolUsage;
ULONG NonPagedPoolUsage;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
typedef struct _OBJECT_ALL_TYPES_INFORMATION {
ULONG NumberOfTypes;
OBJECT_TYPE_INFORMATION TypeInformation;
} OBJECT_ALL_TYPES_INFORMATION, *POBJECT_ALL_TYPES_INFORMATION;
Name决定类型对象名字,类型对象紧跟在每个OBJECT_TYPE_INFORMATION结构后面。下一个OBJECT_TYPE_INFORMATION结构跟在这个Name后面,间隔4个字节。
SYSTEM_HANDLE_INFORMATION结构中的ObjectTypeNumber是TypeInformation数组中的索引。
比较困难的是获取其他进程中句柄的名字。这里有两种命名的可能性。一是通过调用NtDuplicateObject把句柄拷贝到我们的进程中然后命名它。这种方法对某些特殊类型的句柄会失败。但由于它失败的次数比较少,所以我们采用这种方法。
NtDuplicateObject(
IN HANDLE SourceProcessHandle,
IN HANDLE SourceHandle,
IN HANDLE TargetProcessHandle,
OUT PHANDLE TargetHandle OPTIONAL,
IN ACCESS_MASK DesiredAccess,
IN ULONG Attributes,
IN ULONG Options
);
SourceHandle是我们想要拷贝的句柄,SourceProcessHandle是拥有SourceHandle的进程的句柄。TargetProcessHandle是想要拷贝到的进程的句柄,在这里是我们进程的句柄。TargetHandle是指向保存原始句柄拷贝的指针。DesiredAccess应该被设为PROCESS_QUERY_INFORMATION,Attributes和Options设为0。
第二种命名方法对所有句柄都有效,就是使用系统驱动。源代码可以在http://rootkit.host.sk的OpHandle项目里找到。
=====[ 10. 端口 ]==========================================
枚举打开端口最简单的方法是调用AllocateAndGetTcpTableFromStack和AllocateAndGetUdpTableFromStack函数,或者AllocateAndGetTcpExTableFromStack和AllocateAndGetUdpExTableFromStack函数,它们都来自iphlpapi.dll。带Ex的函数从Windows XP才开始有效。
typedef struct _MIB_TCPROW {
DWORD dwState;
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwRemoteAddr;
DWORD dwRemotePort;
} MIB_TCPROW, *PMIB_TCPROW;
typedef struct _MIB_TCPTABLE {
DWORD dwNumEntries;
MIB_TCPROW table[ANY_SIZE];
} MIB_TCPTABLE, *PMIB_TCPTABLE;
typedef struct _MIB_UDPROW {
DWORD dwLocalAddr;
DWORD dwLocalPort;
} MIB_UDPROW, *PMIB_UDPROW;
typedef struct _MIB_UDPTABLE {
DWORD dwNumEntries;
MIB_UDPROW table[ANY_SIZE];
} MIB_UDPTABLE, *PMIB_UDPTABLE;
typedef struct _MIB_TCPROW_EX
{
DWORD dwState;
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwRemoteAddr;
DWORD dwRemotePort;
DWORD dwProcessId;
} MIB_TCPROW_EX, *PMIB_TCPROW_EX;
typedef struct _MIB_TCPTABLE_EX
{
DWORD dwNumEntries;
MIB_TCPROW_EX table[ANY_SIZE];
} MIB_TCPTABLE_EX, *PMIB_TCPTABLE_EX;
typedef struct _MIB_UDPROW_EX
{
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwProcessId;
} MIB_UDPROW_EX, *PMIB_UDPROW_EX;
typedef struct _MIB_UDPTABLE_EX
{
DWORD dwNumEntries;
MIB_UDPROW_EX table[ANY_SIZE];
} MIB_UDPTABLE_EX, *PMIB_UDPTABLE_EX;
DWORD WINAPI AllocateAndGetTcpTableFromStack(
OUT PMIB_TCPTABLE *pTcpTable,
IN BOOL bOrder,
IN HANDLE hAllocHeap,
IN DWORD dwAllocFlags,
IN DWORD dwProtocolVersion;
);
DWORD WINAPI AllocateAndGetUdpTableFromStack(
OUT PMIB_UDPTABLE *pUdpTable,
IN BOOL bOrder,
IN HANDLE hAllocHeap,
IN DWORD dwAllocFlags,
IN DWORD dwProtocolVersion;
);
DWORD WINAPI AllocateAndGetTcpExTableFromStack(
OUT PMIB_TCPTABLE_EX *pTcpTableEx,
IN BOOL bOrder,
IN HANDLE hAllocHeap,
IN DWORD dwAllocFlags,
IN DWORD dwProtocolVersion;
);
DWORD WINAPI AllocateAndGetUdpExTableFromStack(
OUT PMIB_UDPTABLE_EX *pUdpTableEx,
IN BOOL bOrder,
IN HANDLE hAllocHeap,
IN DWORD dwAllocFlags,
IN DWORD dwProtocolVersion;
);
还有另外一种方法。当程序创建了一个套接字并开始监听时,它就会有一个为它和打开端口的打开句柄。我们在系统中枚举所有的打开句柄并通过NtDeviceIoControlFile把它们发送到一个特定的缓冲区中,来找出这个句柄是否是一个打开端口的。这样也能给我们有关端口的信息。因为打开句柄太多了,所以我们只检测类型是File并且名字是\Device\Tcp或\Device\Udp的。打开端口只有这种类型和名字。
如果你看一下iphlpapi.dll里函数的代码,就会发现这些函数同样调用NtDeviceIoControlFile并发送到一个特定缓冲区来获得系统中所有打开端口的列表。这意味着我们要想隐藏端口只需要挂钩NtDeviceIoControlFile函数。
NTSTATUS NtDeviceIoControlFile(
IN HANDLE FileHandle
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG IoControlCode,
IN PVOID InputBuffer OPTIONAL,
IN ULONG InputBufferLength,
OUT PVOID OutputBuffer OPTIONAL,
IN ULONG OutputBufferLength
);
我们感兴趣的成员变量有这几个:FileHandle标明了要通信的设备的句柄,IoStatusBlock指向接收最后完成状态和请求操作信息的变量,IoControlCode是指定要完成的特定的I/O控制操作的数字,InputBuffer包含了输入的数据,长度为按字节计算的InputBufferLength,相似的还有OutputBuffer和OutputBufferLength。
=====[ 10.1 WinXP下使用Netstat OpPorts FPort ]=========================
在Windoes XP获得所有打开端口的列表可以使用一些软件比方OpPorts、FPort和Netstat。
这里程序用IoControlCode0x000120003调用了NtDeviceIoControlFile两次。输出缓冲区在第二次调用时被填满。FileHandle的名字这里总是\Device\Tcp。InputBuffer因不同类型的调用而不同:
1) 为获得MIB_TCPROW数组InputBuffer看起来是这样:
第一次调用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
第二次调用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
2) 为获得MIB_UDPROW数组:
第一次调用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
第二次调用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
3) 为获得MIB_TCPROW_EX数组:
第一次调用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
第二次调用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
4) 为获得MIB_UDPROW_EX数组:
第一次调用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
第二次调用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
你可以看到缓冲区只有少数字节不同。我们现在比较清晰地简要说明一下:
我们感兴趣的调用是InputBuffer[1]为0x04且InputBuffer[17]为0x01。只有使用这些输入数据才能使OutputBuffer里为我们想要的表。如果我们想要获得TCP端口信息我们就把InputBuffer[0]设为0x00,想获得UDP端口信息就把它设为0x01。如果我们还需要额外的输出表(MIB_TCPROW_EX或MIB_UDPROW_EX)我们就在第二次调用里把InfputBufer[16]设为0x02。
如果我们发现使用了这几个参数的调用我们就修改输出缓冲区。获取输出缓冲区中ROW的数量可以很容易根据ROW的大小分开IoStatusBlock结构里的Infotmation变量。隐藏其中一个ROW就会变的容易,只需要用之后的ROW改写他并删掉最后一个ROW。不要忘了修改OutputBufferLength和IoStatusBlock。
=====[ 10.2 Win2k和NT4下使用OpPorts, Win2k下使用FPort ]==========================
我们用IoControlCode0x00210012调用NtDeviceIoControlFile来判断这个拥有类型File和名字\Device\Tcp或\Device\Udp是否是打开端口的句柄。
所以最先我们比较IoControlCode然后是类型和句柄名字。如果这些都符合就接着比较输入缓冲区长度,它应该和结构TDI_CONNECTION_IN长度一样,为0x18。输出缓冲区的结构是TDI_CONNECTION_OUT。
typedef struct _TDI_CONNETION_IN
{
ULONG UserDataLength,
PVOID UserData,
ULONG OptionsLength,
PVOID Options,
ULONG RemoteAddressLength,
PVOID RemoteAddress
} TDI_CONNETION_IN, *PTDI_CONNETION_IN;
typedef struct _TDI_CONNETION_OUT
{
ULONG State,
ULONG Event,
ULONG TransmittedTsdus,
ULONG ReceivedTsdus,
ULONG TransmissionErrors,
ULONG ReceiveErrors,
LARGE_INTEGER Throughput
LARGE_INTEGER Delay,
ULONG SendBufferSize,
ULONG ReceiveBufferSize,
ULONG Unreliable,
ULONG Unknown1[5],
USHORT Unknown2
} TDI_CONNETION_OUT, *PTDI_CONNETION_OUT;
具体判断句柄是不是一个打开端口的方法请参考OpPorts的源代码,在http://rookit.host.sk上可以找到。我们现在来隐藏指定端口。我们已经比较过了InputBufferLength和IoControlCode,现在来比较RemoteAddressLength,对打开端口来说它总是3或4。最后要做的是比较OutputBufferBuffer里的ReceiveTsdus,用网络上的端口和要隐藏的端口列表比较。区别TCP和UDP的做法是句柄的名字不一样。在删除了OutputBuffer、修改IoStatusBlock并返回STATUS_INVALID_ADDRESS后我们就已经隐藏了这个端口了。
=====[ 11. 结束语 ]===============================================
具体细节请参考Hacker defender rootkit version 1.0.0的源代码,在http://rootkit.host.sk和http://www.rootkit.com都可以找到。
在将来我还会加入更多有关的技术。这篇文档的更新版本会改进现有的方法和并加入新的思想。
特别感谢Ratter提供了很多完成这篇文档和Hacker defender代码所需要的技术。
===================================[ End ]==============================
后记:
其实只要我们对Windows的内核有一定程度的了解我们都知道单纯靠挂钩函数是不能真正做到隐藏的,这样做只不过是欺骗操作系统的使用者,却欺骗不了操作系统自己。线程要想被运行就必须获得时间片,将自己加入调度链表中,从而暴露自己。内核维护一组被称为调度程序数据库的数据结构来做出线程调度的决策。其中最重要的结构是调度程序就绪队列(KiDispatckerReadyListHead)。它里面有64个DWORD,分别对应于32个线程优先级的队列,队列包含处于就绪状态的线程,正在等待调度执行。还有两个队列KiWaitInListHead和KiWaitOutListHead保存着处于等待状态的线程。可以很简单的枚举这3个链表中的所有元素从而列出系统中的所有线程。因此要想彻底从Windows系统里“消失”就要从Windows的内核下手(Windows的内核只负责线程调度,其它功能由执行程序组件完成)。这个功能还有待完成。
水平有限,欢迎大家指出错漏之处。
**********************************************************************************************************
绕过内核调度链表进程检测
*************************************************************
一般隐藏进程的方法实际是无法彻底隐藏进程,因为内核调度是基于线程的。下面介绍我实现的一种更隐蔽的隐藏进程的方法。我们知道线程调度内核使用3条调度链表KiWaitInListHead=0x80482258 、KiWaitOutListhead=0x80482808 、KiDispatcherReadyListHead=0x804822e0(这个链表实际是32个LIST_ENTRY的数组,对应32个优先级),事实上还有几个平衡集管理器的链表KiProcessOutSwapListHead 、KiProcessOutSwapListHead 、KiStackInSwapListHead含有进程和线程信息,但它们在绝大多数时候是空链表,因为平衡集管理器只有在页面出错率太高或者空闲列表太少时才被唤醒执行实际工作,所以链表中不会有太多项,而且很快就被执行完。
首先要先在非分页内存中分配对应的LIST_ENTRY结构,然后将原始调度链表内容移动到新链表。在操作链表时要先把IRQL提升到Dispatcher Level,然后请求一个自旋锁.操作结束后释放自旋锁并恢复IRQL(内核里任何涉及到操作内核调度数据结构的例程都是先调用KiLockDispatcherDatabase,操作结束后调用KiUnlockDispatcherDatabase,原理大体和前面说到的操作相似,不同的就是KiUnlockDispatcherDatabase在释放自旋锁并恢复IRQL后若有就绪线程的话就进行环境切换)。系统中用到KiWaitInListHead的例程:KeWaitForSingleObject()、 KeWaitForMultipleObject()、 KeDelayExecutionThread、 KiOutSwapKernelStacks。用到KiWaitOutListHead的例程和KiWaitInListHead的一样。前3个例程都调用了宏KiInsertWaitList。最后一个由于调用了宏RemoveEntryList,所以汇编代码会产生2个0x8048280c。如果不连它们一起替换的话就会出错(系统可以正常运行一段时间,但是在调度新线程
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -