
作者丨selph
分析 MSF Windows/exec Shellcode
Windows/exec cmd="calc.exe"
实验环境
生成
使用Kali Linux生成shellcode:
桌面
前置知识补充
通过寄存器获取模块信息
使用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文件解析,可以使用010 Editor的exe.bt模板来辅助解析
使用010 Editor随便打开一个DLL(一般都有导出表),界面如下:

这里通过下面模板的表格去找对应的偏移即可辅助理解分析中用到的结构,跟着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
通过将当前下一条指令的地址放到栈里
接下来进入函数:
获取指令地址
获取字符串的地址(偏移指向末尾)
保存当前下一行地址,跳转回去
这里通过call+pop ebp的方式获取了shellcode本身的地址,通过硬编码偏移获得shellcode末尾的字符串“calc.exe”,然后入栈3个参数call ebp(ebp的值是刚刚call进来的call指令的下一行)
shellcode使用call+pop可以实现shellcode地址的定位功能
保存寄存器环境
保存栈顶
清空
读取寄存器偏移处:的地址
,当前模块名称
清空
的值给到,对当前模块名计算
和做比较,判断是否是小写
大写字母跳转
小写变大写
循环右移
循环
保存地址
保存当前模块名
读取地址的往后偏移的位置,模块首地址
读取头的扩展头偏移
获取扩展头中数据目录表的导出表的偏移
为则跳转,说明没有导出表
首先保存寄存器环境,通过fs寄存器获得PEB地址,从PEB中找到_LDR_DATA_TABLE_ENTRY结构,找到模块基址用于遍历导出表中的函数名称,寻找函数要用
这里最后判断ecx是否为0,ecx为0意味着没有导出表,如果没有导出表则跳转:
还原保存的当前模块名
还原保存的中的链表节点地址
读取下一个节点
还原搜索导出表之前的环境,然后通过链表读取下一个节点,再次跳转回去搜索下一个节点保存的模块是否有我们需要的函数
如果模块有导出表则不进行跳转,继续执行:
导出表偏移模块基地址导出表位置
保存导出表位置
找到导出名称表地址偏移
找到导出名称表地址
获取导出名称数量
如果数量为则跳转(无名称导出函数或者遍历完整个导出表没找到要找的函数)
存在导出表则计算导出表在模块中的位置,然后解析导出表信息:导出名称表,导出名称数量,如果导出名称数量为0,则表示这里肯定没有我们要调用的函数名称导出的函数,则跳转:
还原保存的当前模块名
还原保存的中的链表节点地址
读取下一个节点
还原搜索导出表之前的环境,然后通过链表读取下一个节点,再次跳转回去搜索下一个节点保存的模块。
如果导出名称数量不为0则继续执行:
作为循环计数,遍历整个导出名称表
读取一个函数名称偏移
读取函数名称地址
清空
取一个字符出来,指向下一个字符,计算函数名
循环右移
对比和
不相等则跳转,意思是遍历整个函数名,到名称末尾时停止循环
,给计算出来的函数名加一个数字
判断是否等于保存的
不相等就跳转
导出表的位置
找到导出序号表偏移
导出序号表偏移模块基址导出序号表地址
是导出序号偏移,这里是计算导出序号表的索引
导出地址表偏移
导出地址表地址
按照导出序号获取导出函数地址偏移
拿到导出函数地址
保存导出函数到栈里
堆栈平衡,把之前的都
构造返回地址,返回到刚刚入栈的地方的下一行,去找下一个函数来执行
执行函数
这里ecx是导出名称数量,同时也作为索引去搜索导出名称表中的函数名称,对每一个找到的导出名称进行Hash计算,然后与栈中保存的我们要找的函数的Hash进行比对,如果找不到,则找下一个函数,如果函数找完了,则找下一个模块
如果找到了,则pop导出表地址给eax,再次解析导出表信息:导出序号表,导出地址表,从导出名称表中获得的索引去获取导出序号表中对应的序号,通过找到的导出序号去导出地址表找到对应的导出函数地址,保存到eax里
拿到导出函数地址之后,堆栈平衡还原到搜索函数之前的位置,然后自己构造返回地址,通过push jmp来模拟call,push的返回地址是我们构造的刚刚push函数hash后面的call留下的地址
执行完函数会返回回去:
跳转去找到函数并执行
看返回结果是否小于
小于则跳转
判断结尾是不是
不是则跳转,实际上是判断是退出线程还是退出进程
参数:
调用
字符串
然后接下来走同样的途径,去依次调用接下来要调用的函数:GetVersion,ExitProcess
这里shellcode最后这个call ebp之后的内容,不是指令,是我们调用函数的字符串参数“calc.exe”
到这里shellcode整个工作流程就是这些了,本例中,执行结果就是弹出计算器
执行流程总结
Shellcode执行流程总结:
_TEB找到_PEB
_PEB_PEB_LDR_DATA
_PEB_LDR_DATA_LDR_DATA_TABLE_ENTRY
完整反汇编分析注释
通过将当前下一条指令的地址放到栈里
保存寄存器环境
保存栈顶
清空
读取寄存器偏移处:的地址
,当前模块名称
清空
的值给到,对当前模块名计算
和做比较,判断是否是小写
大写字母跳转
小写变大写
循环右移
循环
保存地址
保存当前模块名
读取地址的往后偏移的位置,模块首地址
读取头的扩展头偏移
获取扩展头中数据目录表的导出表的偏移
为则跳转,说明没有导出表
导出表偏移模块基地址导出表位置
保存导出表位置
找到导出名称表地址偏移
找到导出名称表地址
获取导出名称数量
如果数量为则跳转(无名称导出函数或者遍历完整个导出表没找到要找的函数)
作为循环计数,遍历整个导出名称表
读取一个函数名称偏移
读取函数名称地址
清空
取一个字符出来,指向下一个字符,计算函数名
循环右移
对比和
不相等则跳转,意思是遍历整个函数名,到名称末尾时停止循环
,给计算出来的函数名加一个数字
判断是否等于保存的
不相等就跳转
导出表的位置
找到导出序号表偏移
导出序号表偏移模块基址导出序号表地址
是导出序号偏移,这里是计算导出序号表的索引
导出地址表偏移
导出地址表地址
按照导出序号获取导出函数地址偏移
拿到导出函数地址
保存导出函数到栈里
堆栈平衡,把之前的都
构造返回地址,返回到刚刚入栈的地方的下一行,去找下一个函数来执行
执行函数
还原保存的当前模块名
还原保存的中的链表节点地址
读取下一个节点
获取指令地址
获取字符串的地址(偏移指向末尾)
保存当前下一行地址,跳转回去
跳转去找到函数并执行
看返回结果是否小于
小于则跳转
判断结尾是不是
不是则跳转,实际上是判断是退出线程还是退出进程
参数:
调用
字符串
完整反汇编分析注释截图版

总结
该shellcode的主要流程其实是下面那一小段,入栈函数参数和函数Hash,然后调用函数去搜索函数地址并调用,然后再用相同的方式调用下一个函数,直到完成shellcode执行的功能,通过对本例的分析,可以很清晰明了地了解shellcode是如何获取函数地址的,以及如何调用的,也算是一次不错的反汇编练习
最后,感谢大家的浏览,如有问题欢迎师傅们指出、探讨与交流~
参考资料
RE Corner - scdbg download (sandsprite.com)
(http://sandsprite.com/blogs/index.php?uid=7&pid=152)
走进shellcode - 安全客,安全资讯平台 (anquanke.com)
(https://www.anquanke.com/post/id/264883)
LODS/LODSB/LODSW/LODSD/LODSQ — Load String (felixcloutier.com)
(https://www.felixcloutier.com/x86/lods:lodsb:lodsw:lodsd:lodsq)
汇编跳转指令: JMP、JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等_zmmycsdn的博客-CSDN博客_汇编