环境搭建
- 把防火墙关闭了就好
- 安装域控
实验环境及工具如下:
server 2012 sp0 datacenter (装了域控,不是r2版本啊)
windbg
VirtualKD-3.0
wireshark
工具利用复现
首先第一次探测的时候做了如下事情:
- 获取目标系统版本信息
- 尝试可以访问的管道
- 探测目标系统的架构(32位还是64位)
- 还会输出可以利用什么工具,那几个工具的检测代码应该是类似的
接下来开始漏洞利用,先生成shellcode
之后设置为上面生成shellcode的位置
最后即可安装后门
用Doublepulsar检测一下,确实成功了
根据输出信息,我们可以看到利用步骤如下:
- 利用信息泄露:先找到CONNECTION结构体,之后找到SRV全局指针,最后就找到了 Transaction2Dispatch tables的位置
- 查找可执行的内存
- 复制backdoor shellcode到上面找到的内存
- 触发stub分配器,覆盖backdoor函数指针
- 触发 DOUBLEPULSAR的安装
初步漏洞分析
我将shellcode前面都改为CC,跟着windbg就乖乖断下来了,刚好断在shellcode起始位置
1 | kd> g |
那bp srv!ExecuteTransaction看看什么情况
第一个包就暂停下来了,应该很多包都会暂停,那先不管
1 | kd> g |
先看看ida看看srv!ExecuteTransaction+0x2cf前一个指令是什么
原来是SrvTransaction2DispatchTable里面的函数
1 | PAGE:00000000000586C8 call rva SrvTransaction2DispatchTable[rdx+rax*8] |
这样我就可以大胆推测,应该是改写了这个函数table里面的值
下面为table里面的函数列表
1 | data:000000000002A920 SrvTransaction2DispatchTable dq offset SrvSmbOpen2 |
我们对bp srv!ExecuteTransaction+0x2c8,即 call rva SrvTransaction2DispatchTable[rdx+rax*8]
下断
,执行几次我们就到达了调用shellcode处,可以看到.data:000000000002A990 dq offset SrvTransactionNotImplemented
被覆盖掉了,我们可以在这里下断看看是什么时候被写入的
1 | kd> dqs SrvTransaction2DispatchTable l12 |
结果发现是SrvSmbTransactionSecondary时调用了memmove
1 | kd> g |
是在下面这里
那么就是利用这里进行任意地址的写操作,到这了没什么思路,到底具体是什么原因导致这里任意地址写呢
我们从最开始分析吧
判断系统位数
通过发送bind,看看收到的bind ack那里来确定系统位数
也可以看到攻击端是32位的
信息泄露数据包体现
发了好多Trans Request,MID是递增的,所以应该在内存构建了多个相邻的transaction
获取到了CONNECTION结构的地址后就用这地址去发送Request,这里的数据包是嵌套了很多smb数据,其中我们的
CONNECTION struct: 0xFFFFFA801D4D3B90 是在第3个smb里面(不算第一个的话)
- 收到SRV global data pointer
- 发了很多尝试包,都是两个两个发的,下面这个是泄露那个table的最后一个包
- 收到这个包就确定了table的地址,应该是根据这个table前面有很多0xfe,还有table中后面两个函数指针是保留指针,所以地址是一样的,猜测是这样判断的
6.最后就是判断读写执行的内存的返回包,其实后来调试他是在寻找nt!KxUnexpectedInterrupt0,这个地址就是RWX地址
信息泄露原理
原理简述
这里面用了两种信息泄露的点
发送nt rename包,在RestartTransactionResponse的时候利用下面的这一句覆盖另一个transaction的OutData指针,这个用于泄露CONNECTION地址
1
memmove(pNtRenameParameters, (const void *)(*(_QWORD *)(transaction + 0x88) + dataDisplacement), DataCount);// 0x88偏移为out Data
第二种是利用SrvSmbTransactionSecondary下面这句,泄露其他地址
1 | memmove( |
其实我觉得别的文章说的类型混淆,可能找transaction的时候找到的是WriteAndX的,实际上更重要的是dataDisplacement的偏移,而且数据包中很多利用的MID都是0,应该只有两次是利用了WriteAndX同样的MID
既然可以覆盖OutData指针,那么也可以覆盖InData指针,就可以达到任意地址写的目的了
通过SrvFindTransaction看信息泄露
下断点 bp SrvFindTransaction+0xbb "r r15;dq rsi+0xc0;gc"
1 | ....... |
那么可以看到 fffff8a002b4e170+ 0x88 = fffff8a002b4e1f8
这个地址就是被利用的地址(即fffff8a002ba1270地址这个transaction的InData指针的加一个偏移后,覆盖了另一个transaction的OutData指针,从而达到信息泄露的目的),0x88为Data Displacement
泄露CONNECTION地址
在这一句已经开始发送泄露的数据包,去泄露CONNECTION的地址
数据包如下:(可以看到是NT RENAME的trans数据包)
处理第一个包
接下来就会进入srv!SrvSmbNtRename函数
1 | kd> g |
这函数是从工具程序和数据包联合启发而找到的
SrvSmbNtRename函数在SrvNtTransactionDispatchTable中,通过偏移调用实现的
接下来我们动态跟踪
里面对FID进行Verify
返回值为0xfffffa801dfb39d0(那是某一次调试记录),之后函数就返回了
整个流程如下
出来后进入这个
跟数据包中下面这个是相关的
最后在SrvCompleteExecuteTransaction调用SrvStartSend返回smb数据包
1 | kd> p |
还有一个问题没弄明白的就是rename的数据到底有什么用,暂时猜测只是填充
处理第二个包secondary包
由于是Nt trans 的包,所以调用的是srv!SrvSmbNtTransactionSecondary
1 | kd> g |
在下面这条赋值中发现上面截图中数据包中的数据
1 | kd> p |
下面调用SrvFindTransaction,找到了,返回了0xfffff8a002c42010
出了SrvFindTransaction函数调试到下面
根据数值和数据包可以大胆猜测偏移0x10的是Parameter Offset,0x1c的是Data Offset
下面就进入memmove那里了
由于ParameterCount为0,就只进入第二个memmove那里了
先看一下参数,复制的大小是DataCount的值0x150,目标是fffff8a002c46050
1 | kd> p |
目标地址是第一次trans Request的数据(这样会被覆盖吧)
那么这里DataDisplacementCopy为0,导致覆盖了前一个transaction的InData指向的地址
但是发现其实前150个字节是一样的,汗~
接下来对对应的transaction的对应字段加上相应的值
到这其实有个问题,就是实际上transaction的数据并没有增大,这个datacout应该是虚增了,但看不出什么问题
1 | fffff880`0507deb6 4401b6a4000000 add dword ptr [rsi+0A4h],r14d ds:002b:fffff8a0`02c420b4=000010c0 |
再下面就到了SrvDereferenceConnection,没进入if,直接出来了
SrvSmbNtTransactionSecondary出来了就到了SrvEndSmbProcessing
一次偶然发现第一次nt response和第二次的nt response内存中的地址是一样的
1 | kd> g |
1 | kd> g |
所以我在获取了第一次的地址后,对c0偏移位置下写入断点就好了,果然断下来了,原来调用了RestartTransactionResponse函数
再一步步跟原来调用了RestartTransactionResponse函数,终于找到点子上了
首先将第一个response包的DataCount给到一个局部变量,这个局部变量其实就是下面要用到的dataDisplacement
之后便是一些赋值,计算操作(这个对整个分析的理解是很重要的,当然这个过程我也结合了数据包中的数据)
接下来就将第一次的nt response的OutData再偏移dataDisplacement(0x10b8)
看看复制的源地址是什么,这个地址刚好是第一次nt response的OutData的后面
1 | kd> dq poi(rbx+88)+10b8 l21 |
这里我们看到了熟悉的fffffa801d4d2b90
,这个就是CONNECTION结构地址
再看看这个地址有什么符号,我们看到了srv!SrvGlobalSpinLocks+0x70,这个应该就是SRV global data pointer
1 | kd> dqs fffffa80`1d4d2b90 |
泄露 SRV global data pointer
经过一番调试,费了一上午,突然想到,既然已经知道CONNECTION的地址了,那么我对CONNECTION的那个偏移下读取断点不就行了
1 | bp srv!RestartTransactionResponse+0x288 "dqs poi(rdx+78);ba r8 poi(rdx+78)+38" |
由于ba r8 poi(rdx+78)+38这个断点断了很多,最后断到了memmove,结果一时手快,直接g,错过了
后来还是辛苦的断了下来,原来是SrvCompleteExecuteTransaction调用的memmove
1 | ...... |
可以再下个断点确认一下
1 | kd> g |
这个函数跟了下,复制大小是0x40,复制的起始地址是正是CONNECTION的其实地址
那应该是覆盖了附近的transaction的OutData指针了
在SrvSmbTransactionSecondary+0x2cf 下个断点(即里面的memmove那里)
1 | kd> bp srv!SrvSmbTransactionSecondary+0x2cf ".if (@r8 > 0x8) {r rdx;r r8;db rdx l100;.echo ----------}.else{gc}" |
其实我们注意到复制的源地址的第一个8字节就是泄露出来的CONNECTION地址
目标地址如下:而这个应该就是“另一个transaction”OutData指针的位置
1 | kd> dq rcx |
注释后的ida代码,
再结合下面的数据包中的数据,首先DataCount是0x28(40),就是上面memmove的大小
再看DataOffset是0x42,其实这个偏移刚好是数据包中CONNECTION的地址
还有一个DataDisplacement,这是控制能够覆盖到附近transaction的OutData的保证
泄露Transaction2Dispatch Table
这个是在SRV global data pointer的地址的基础上在前面0x1000偏移处开始寻找
以某次找到的SRV global data pointer地址: 0xFFFFF8800501AF28为例
请求的地址依次如下:(每次递增0x200)
1 | 0xFFFFF88005019F28 |
从下面可以看到利用的目标地址都是相同的fffff8a0029471f8
1 | kd> g |
猜想是根据Transaction2Dispatch Table前面的特征确认的吧,因为前面搜到的包都没有0xfefefefe的,只有Transaction2Dispatch Table有,假如我写代码可能会这么判断,还有就是这个表后面的两个函数指针是保留指针,所以地址相同,这个可以进一步判断
一步步获得执行权限
注:下面可能为多次调试,地址会变了,但最后一个字节应该是一样的
确认可执行内存(其实是查找nt!KxUnexpectedInterrupt0)
第一次请求
1 | kd> g |
读取的核心内容如下:
1 | fffffa80`1ab79468 fffffa80`1d48a600 |
根据命令行的输出,这个是PreferredWorkQueue的地址
那么这个fffffa801ab79468
地址是怎么来的呢,经过下个巧妙的断点,再ctrl+f,就找到了
断点如下
1 | bp srv!SrvSmbTransactionSecondary+0x2cf ".if (@r8 > 0x8) {r rcx;r rdx;r r8;dqs rdx;.echo ***************************;dqs poi(rdx);.echo -----------------------}.else{gc}" |
我们发现fffffa801ab79468
就在CONNECTION地址的0x78偏移处
1 | fffffa80`1ab793f0 0000004a`04700202 |
之后就会泄露PreferredWorkQueue+0x198:即读取fffffa801d48a798
的值
1 | kd> g |
而获取到的 fffffa801d48e040正是IrpThread
KProcess的值便是IrpThread+0x220处的值
1 | kd> dqs fffffa801d48e040+0x220 |
ProcessListEntry.Blink是在KProcess+0x240处(到这才有符号,汗,没控制台的输出那就很难搞了)
1 | kd> dqs fffffa80`18c5f980+0x240 l1 |
接下来不断向后搜寻从fffff8001ac20000
开始向前寻找,在fffff8001ac1e000
找到了Nt的基址(Base of Nt)
1 | fffff800`1ac20000 nt!XpressDecodeCreate+0x20 |
那是怎么确定这就是Nt的基址呢?(感觉有点不靠谱,就一个MZ)
在之后就泄露下面两个地址(我猜测nt!KxUnexpectedInterrupt0是通过Nt的基址算出来的)
1 | fffff800`1ac1e258 nt!`string' <PERF> (nt+0x258) |
而nt!KxUnexpectedInterrupt0正是可读可写地址
写入一些数据及SrvTransaction2DispatchTable函数指针改写
首先将fffff8001ae8f000
覆盖一个transaction的InData指针,为写入shellcode做准备
1 | kd> r |
在数据包体现如下:
之后将一些数据复制到fffff8001ae8f000(nt!KxUnexpectedInterrupt0)
1 | kd> p |
数据包体现
复制了上面这些数据后,再将目标transaction的InData指针替换为fffff8800501a990
:srv!SrvTransaction2DispatchTable+0x70,为改写这个表做准备
1 | kd> p |
接下来就可以修改srv!SrvTransaction2DispatchTable+0x70的位置为nt!KxUnexpectedInterrupt0了
1 | kd> g |
一开始不知道为什么复制这些数据到那里,又要修改SrvTransaction2DispatchTable+0x70,后来下断点发现调用了这里的代码,原来这个数据也算是一个小shellcode
1 | nt!KxUnexpectedInterrupt0: |
原来这里调用了分配pool内存的函数
1 | fffff800`1ae8f01b ff10 call qword ptr [rax] ds:002b:fffff800`1af14a30={nt!ExAllocatePoolWithTag (fffff800`1ae8d040)} |
大小是e4d(这个跟下面shellcode的大小是一致的)
1 | kd> r rcx;r rdx;r r8 |
最后将分配的内存首地址复制给nt!KxUnexpectedInterrupt0+0x2d
1 | nt!KxUnexpectedInterrupt0+0x25: |
之后就会读取nt!KxUnexpectedInterrupt0+0x2d的值,再向这个读取到的地址写入shellcode。(调试过程也看到要泄露nt!KxUnexpectedInterrupt0+0x2d的值,到这里就明白了)
shellcode的写入
下断点:
1 | bp srv!SrvSmbTransactionSecondary+0x2cf "r r8;dqs rdx;.echo *****;dqs rcx;.echo -------------------;gc" |
首先将一个transaction的InData改写为fffffa8018e8e010
1 | r8=0000000000000008 |
之后向fffffa8018e8e010
复制shellcode(我将shellcode前面改为0xcc开头了,好辨别)
1 | r8=0000000000000e4d |
shellcode前面的字节是软件作者自己加到shellcode前面的,其实就是跳转到shellcode,这样不知有什么好处
触发shellcode的执行
先将那个transaction的InData改为srv!SrvTransaction2DispatchTable+0x70
1 | r8=0000000000000008 |
之后在srv!SrvTransaction2DispatchTable+0x70处写入shellcode地址(fffffa8018e8e010
)
1 | r8=0000000000000008 |
最后发送一个command为0x32的包,触发那个表中的函数调用
漏洞分析总结
- 本工具主要利用SrvSmbTransactionSecondary中的memmove从而覆盖另一个transaction的OutData指针和InData地址从而到达任意地址写和任意地址读的漏洞
- 也利用了nt rename,之后利用RestartTransactionResponse的memmove覆盖另一个OutData指针
- 通过改写SrvNtTransactionDispatchTable的0x70偏移达到执行任意代码的目的
- 还有一段小shellcode,去分配pool内存
- 之后泄露pool内存的地址
- 最后将真正的shellcode复制到那个内存,最后触发shellcode执行
分析smb漏洞的一些总结
- nt trans request是用nt的函数来处理的,否则是不带nt的函数来处理(例子 SrvSmbNtTransactionSecondary和SrvSmbTransactionSecondary)
- nt trans request最后发送给客户端的时候是用SrvStartSend,不带nt的request使用的是SrvStartSend2
- 在64位系统的情况下 transaction的结构如下:
1 | struct transaction{ |
1 | SrvSmbTransactionSecondary的第一个参数偏移 |
- CONNECTION地址的0x78偏移处储存的是PreferredWorkQueue
- PreferredWorkQueue+0x198偏移处储存的是IrpThread
- IrpThread+0x220偏移储存的是KProcess
- KProcess+0x240偏移存储的是nt!KiProcessListHead(ProcessListEntry.Blink)
- Nt的基址在nt!KiProcessListHead前面,本工具从nt!KiProcessListHead-0x2cfc80开始寻找
- 怎么确认找到了Nt的基址呢?就凭两个字节,就是MZ头,😄
参考资料
https://blogs.technet.microsoft.com/srd/2017/07/13/eternal-synergy-exploit-analysis/
http://blogs.360.cn/360safe/2017/04/19/eternalromance-analyze/