图三
以上界面帮助管理员设置各种脚本映射,我们来看每一种影射应该怎样设置:
1)Disable support Active Server Pages(ASP),选择这种设置将使IIS不支持ASP功能;可以根据网站具体情况选择,一般不选择此项,因为网站一般要求运行ASP程序;
2)Disable support Index Server Web Interface(.idq,.htw,.ida),选择这一项将不支持索引服务,具体就是不支持.idq,.htw,.ida这些文件。我们先来看看到底什么是索引服务,然后来决定取舍。索引服务是IIS4中包含的内容索引引擎。你可以对它进行ADO调用并搜索你的站点,它为你提供了一个很好的web 搜索引擎。如果你的网站没有利用索引服务对网站进行全文检索,也就可以取消网站的这个功能,取消的好处是:1)减轻系统负担;2)有效防止利用索引服务漏洞的病毒和黑客,因为索引服务器漏洞可能使攻击者控制网站服务器,同时,暴露网页文件在服务器上的物理位置(利用.ida、.idq)。因此,我们一般建议在这一项前面打勾,也就是取消索引服务;
3)Disable support for Server Side Includes(.shtml,.shtm,.stm),取消服务器端包含;先来看看什么叫服务器端包含,SSI就是HTML文件中,可以通过注释行调用的命令或指针。SSI 具有强大的功能,只要使用一条简单的SSI 命令就可以实现整个网站的内容更新,动态显示时间和日期,以及执行shell和CGI脚本程序等复杂的功能。一般而言,我们没有用到这个功能,所以,建议取消;取消可以防止一些IIS潜在地漏洞;
4)Disable for Internet Data Connector(.idc),取消Internet数据库连接;先看Internet数据库连接的作用,它允许HTML页面和后台数据库建立连接,实现动态页面。需要注意的是,IIS4和IIS5中基本已经不使用idc,所以,建议在此项打勾,取消idc;
5)Disable support for Internet Printing (.printer),取消Internet打印;这一功能我们一般没有使用,建议取消;取消的好处是可以避免.printer远程缓存溢出漏洞,这个漏洞使攻击者可以利用这个漏洞远程入侵IIS 服务器,并以系统管理员(system)身份执行任意命令;
6)Disable support for .HTR Scripting(.htr),取消htr映射;攻击者通过htr构造特殊的URL请求,可能导致网站部分文件源代码暴露(包括ASP),建议在此项前面打勾,取消映射;
理解以上各项设置以后,我们可以根据本网站情况来决定取舍,一般网站除了ASP要求保留以外,其他均可以取消,也就是全消第一项前面的勾,其他全部打勾,按【下一步】按钮,出现以下界面(图四)
图四
以上界面设置可以让管理员选择一些IIS默认安装文件的保留与否,我们来看怎样选择:
1)Remove sample web files,删除web例子文件;建议删除,因为一般我们不需要在服务器上阅读这些文件,而且,这些文件可能让攻击者利用来阅读部分网页源程序代码(包括ASP);
2)Remove the Scripts vitual directory,删除脚本虚拟目录;建议删除;
3)Remove the MSDAC virtual directory,删除MSDAC虚拟目录,建议删除;
4)Disable Distribauted Authoring and Versioning(WebDAV),删除WEBDAV,WebDav主要允许管理者远程编写和修改页面,一般我们不会用到,建议删除,删除的好处是可以避免IIS5的一个WebDav漏洞,该漏洞可能导致服务器停止。
5)Set file permissions to prevent the IIS anouymous user from executing system utilities(such as cmd.exe,tftp.exe),防止匿名用户运行可执行文件,比如cmd.exe和tftp.exe;建议选择此项,因为红色代码和尼姆达均利用了以上所说的匿名执行可执行文件的功能;
6)Set file permissions to prevent the IIS anouymous user from writing to content directories,防止匿名用户对目录具有写权限,这个不要解释,建议选择;
设置以上选项以后,按【下一步】按钮,出现以下界面(图五):
3。高级篇:NT/2000的高级安全设置
1.禁用空连接,禁止匿名获得用户名列表
Win2000的默认安装允许任何用户通过空用户得到系统所有账号/共享列表,这个本来是为了方便局域网用户共享文件的,但是一个远程用户也可以得到你的用户列表并使用暴力法破解用户密码。很多朋友都知道可以通过更改注册表Local_Machine\System\CurrentControlSet\Control\LSA-RestrictAnonymous = 1来禁止139空连接,实际上win2000的本地安全策略(如果是域服务器就是在域服务器安全和域安全策略中)就有这样的选项RestrictAnonymous(匿名连接的额外限制),这个选项有三个值: 0:None. Rely on default permissions(无,取决于默认的权限 1 :Do not allow enumeration of SAM accounts and shares(不允许枚举SAM帐号和共享) 2:No access without explicit anonymous permissions(没有显式匿名权限就不允许访问) 0这个值是系统默认的,什么限制都没有,远程用户可以知道你机器上所有的账号、组信息、共享目录、网络传输列表(NetServerTransportEnum等等,对服务器来说这样的设置非常危险。 1这个值是只允许非NULL用户存取SAM账号信息和共享信息。 2这个值是在win2000中才支持的,需要注意的是,如果你一旦使用了这个值,你的共享估计就全部完蛋了,所以我推荐你还是设为1比较好。 好了,入侵者现在没有办法拿到我们的用户列表,我们的账户安全了
2。禁止显示上次登陆的用户名HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\winlogon项中的Don’t Display Last User Name串数据改成1,这样系统不会自动显示上次的登录用户名。将服务器注册表HKEY_LOCAL_ MACHINE\SOFTWARE\Microsoft\
WindowsNT\CurrentVersion\Winlogon项中的Don';t Display Last User Name串数据修改为1,隐藏上次登陆控制台的用户名。其实,在2000的本地安全策略中也存在该选项
Winnt4.0修改注册表:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\Current Version\Winlogon 中增加DontDisplayLastUserName,将其值设为1。
NTSTATUS NtQueryDirectoryFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG FileInformationLength,
IN FILE_INFORMATION_CLASS FileInformationClass,
IN BOOLEAN ReturnSingleEntry,
IN PUNICODE_STRING FileName OPTIONAL,
IN BOOLEAN RestartScan
);
=====[ 3.2 NtVdmControl ]========================================
不知什么原因DOS的枚举NTVDM能够通过函数NtVdmControl也能获得文件的列表。
NTSTATUS NtVdmControl(
IN ULONG ControlCode,
IN PVOID ControlData
);
ConcrolCode标明了在缓冲区ControlData中申请数据的子函数。如果ControlCode为VdmDiretoryFile那么这个函数的功能将和FileInformation设置为FileBothDirectoryInformation的函数NtQueryDirectoryFile功能一样。
#define VdmDirectoryFile 6
这时的ControlData的用法就和FileInformation一样。这里唯一的不同就是我们不知道缓冲区的长度。所以我们需要手动来计算它的长度。我们把所有记录的NextEntryOffset和最后一个记录的FileNameLength还有0X5E(最后一个记录除去文件名的长度)。隐藏的方法和前面提到的使用NtQueryDirectoryFile的方法一样。
=====[ 4. 进程 ]========================================
各种进程信息是通过NtQuerySystemInformation获取的。
NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
=====[ 5.1 NtEnumerateKey ]===============================
因为注册表的结构我们不能请求某个指定部分所有键的列表。我们只能在注册表某个部分通过查询指定键的索引以获得它的信息。这里提供了NtEnumerateKey。
NTSTATUS NtEnumerateKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_INFORMATION_CLASS KeyInformationClass,
OUT PVOID KeyInformation,
IN ULONG KeyInformationLength,
OUT PULONG ResultLength
);
KeyHandle是已经用索引标明我们想要从中获取信息的子键的句柄。KeyInformationClass标明了返回信息类型。数据最后写入KeyInformaiton缓冲区,缓冲区长度为KeyInformationLength。写入的字节数由ResultLength返回。
我们需要意识到的最重要的东西是如果我们隐藏了某个键,在这个键之后的所有键的索引都会改变。因为我们是通过高位的索引来获取键的信息,并通过低位的索引来请求这个键。所以我们必须记录之前有多少个记录被隐藏,然后返回正确的值。
让我们来看个例子。假设我们在注册表中有一些键名字是A,B,C,D,E和F。它们的索引从0开始,也就是说索引4对应键E。现在我们如果想要隐藏键B,被挂钩过的应用程序用索引4调用NtEnumerateKey时我们应该返回F键的信息因为有一个索引改变了。现在问题是我们不知道是否会有索引被改变。如果我们不注意索引的改变而对于索引4的请求仍然返回键E而不是键F的话,很有可能在我们用索引1请求时什么都返回不了或者返回键C。这两种情况都会导致错误。这就是为什么我们要注意索引的改变。
现在如果我们通过用索引0到Index重新调用函数来记录转移我们可能会等待一段时间(在1GHz处理器上普通的注册表就得等10秒种那么长的时间)。所以我们不得不想出一种更加巧妙的方法。
我们知道键是按字母排序的(除了引用外)。如果我们忽略引用(我们不需要隐藏)我们能使用以下方法记录改变。我们通过字母排序列出我们想要隐藏的键名的列表(使用RtlCompareUnicodeString),然后当应用程序调用NtEnumerateKey时我们不需要用不可变的变量重新调用它,而能够找到用索引标明的记录的名字。
NTSTATUS RtlCompareUnicodeString(
IN PUNICODE_STRING String1,
IN PUNICODE_STRING String2,
IN BOOLEAN CaseInSensitive
);
String1和String2是将要比较的字符串,CaseInSensitive在不忽略大小写时被设置为True。
函数结果描述String1和String2的关系:
result > 0: String1 > String2
result = 0: String1 = String2
result < 0: String1 < String2
现在我们需要找到一个边缘项。我们在列表中对用索引标明的键按字母比较名字。边缘项是在我们列表中最后一个较短的名字。我们知道转移最多是我们列表中边缘项的数量。但并不是所有我们列表中的项都是注册表中有效的键。所以我们不得不请求我们列表中达到边缘项的所有的在注册表中这个部分的项。这些通过调用NtOpenKey来完成。
NTSTATUS NtOpenKey(
OUT PHANDLE KeyHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
);
KeyHandle是高位的键的句柄,我们使用NtEnumerateKey的这个值。DesaireAccess是访问权力。KEY_ENUMERATE_SUB_KEYS是它的正确的值。ObjectAttributes描述了我们要打开的子键(包括了它的名字)。
#define KEY_ENUMERATE_SUB_KEYS 8
如果NtOpenKey返回0表示打开成功,意味着这个来自我们列表中的键是存在的。被打开的键通过NtClose来关闭。
NTSTATUS NtClose(
IN HANDLE Handle
);
=====[ 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(进程环境块)。
NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
我们把ProcessInformationClass设置为ProcessBasicInformation,然后PROCESS_BASIC_INFORMATION结构会返回到ProcessInformation缓冲区中,大小为给定的ProcessInformationLength。
#define ProcessBasicInformation 0
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
KAFFINITY AffinityMask;
KPRIORITY BasePriority;
ULONG UniqueProcessId;
ULONG InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;
PebBaseAddress就是我们要寻找的东西。在PebBaseAddress+0C处是PPEB_LDR_DATA的地址。这些通过调用NtReadVirtualMemory来获得。
NTSTATUS NtReadVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
OUT PVOID Buffer,
IN ULONG BufferLength,
OUT PULONG ReturnLength OPTIONAL
);
变量和NtWriteVirtualMemory的很相似。
在PPEB_LDR_DATA+01C处是InInitializationOrderModuleList的地址。它是被加载进进程的动态链接库的列表。我们只对这个结构中的一些部分感兴趣。
typedef struct _IN_INITIALIZATION_ORDER_MODULE_LIST {
PVOID Next,
PVOID Prev,
DWORD ImageBase,
DWORD ImageEntry,
DWORD ImageSize,
...
);
Next是指向下一个记录的指针,Prev指向前一个,最后一个记录的会指向第一个。ImageBase是内存中模块的地址,ImageEntry是模快的入口点,ImageSize是它的大小。
对所有我们想要挂钩的库我们需要获得它们的ImageBase(比方调用GetModuleHandle或者LoadLibrary)。然后把这个ImageBase和InInitializationOrderModuleList的ImageBase比较。
现在我们已经为挂钩准备就绪。因为我们是挂钩正在运行的进程,所以可能我们正在改写代码的同时代码被执行,这时就会导致错误。所以首先我们就得停止目标进程里的所有线程。它的所有线程列表可以通过设置了SystemProcessAndThreadInformation的NtQuerySystemInformation来获得。有关这个函数的描述参考第4节。但是还得加入SYSTEM_THREADS结构的描述,用来保存线程的信息。
typedef struct _SYSTEM_THREADS {
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER CreateTime;
ULONG WaitTime;
PVOID StartAddress;
CLIENT_ID ClientId;
KPRIORITY Priority;
KPRIORITY BasePriority;
ULONG ContextSwitchCount;
THREAD_STATE State;
KWAIT_REASON WaitReason;
} SYSTEM_THREADS, *PSYSTEM_THREADS;
对每个线程调用NtOpenThread获取它们的句柄,通过使用ClientId。
NTSTATUS NtOpenThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId
);
我们需要的句柄被保存在ThreadHandle。我们需要把DesiredAccess设置为THREAD_SUSPEND_RESUME。
#define THREAD_SUSPEND_RESUME 2
ThreadHandle用来调用NtSuspendThread。
NTSTATUS NtSuspendThread(
IN HANDLE ThreadHandle,
OUT PULONG PreviousSuspendCount OPTIONAL
);
被挂起的进程就可以被改写了。我们按照"挂钩Windows API"里3.2.2节里描述的方法处理。唯一的不同是使用其它进程的函数。
挂钩完后我们就可以调用NtResumeThread恢复所有线程的运行。
NTSTATUS NtResumeThread(
IN HANDLE ThreadHandle,
OUT PULONG PreviousSuspendCount OPTIONAL
);
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项目里找到。
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并且名字是DeviceTcp或DeviceUdp的。打开端口只有这种类型和名字。
如果你看一下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。