0%

2019-01-24-一段简单的Shellcode内存加载PE的分析

一段简单恶意代码注入的Shellcode部分,主要功能为内存解析加载,帮朋友简单看了下,写了个简单的分析,顺便博客记录下,懒IDB里就没详细标注等,仅做个简单的记录。

简介

    Shellcode代码因为其一般都是加载到动态申请的内存中,内存地址不固定,这就要求Shellcode代码中不能包含绝对地址引用的代码,如:

    实际的Shellcode代码因为地址不固定,如果出现图中向固定地址拷贝数据的指令,往往会因为该地址处不存在可写内存而导致执行崩溃,为了避免这种情况,Shellcode本身往往在最开始的指令就需要首先获取到自身所加载的内存地址,即重定位过程,以附件所给的shellcode样本为例:

    第一条call指令是调用的自身下一条指令(这条call指令本身占5个字节,其中E8代表Call指令的机器码,后四字节值为00000000代表相对本条指令结束地址的偏移,既是下一条指令,IDA识别成Call $5则代表相对指令首地址0偏移5字节),在call指令执行时会首先将返回地址压栈,再跳转到第二条指令执行,而第二条指令pop eax则是将上一条call指令压入栈顶的返回地址值弹出赋值给eax,此时eax的值即为这段shellcode所在的内存首地址+5,第三条指令sub eax,5执行完之后eax寄存器值即为shellcode在内存中的起始地址。这就是最典型的利用call xxx;pop bbb类型指令完成重定位的指令特征。 ### 分析

    因shellcode无法直接运行,为了动态调试Shellcode需要提供其运行环境,可以选择自己编程加载,也可以将Shellcode数据使用16进制编辑器拷贝到其他程序中直接调试(这种方式要求拷贝进去的程序空间足够容纳Shellcode),示例Shellcode样本体积较大,为了方便采用MDebug调试器直接加载调试(MDebug的32位调试器相对OD等使用界面比较丑陋,插件支持的也不多,如果再麻烦点的Shellcode还是前几种方式加载再使用OD调试方便,MDebug调试器的优势在于加载Shellcode方便并且支持64位代码):

    选择shellcode文件打开查看代码:

    在当前调试状态Shellcode加载内存基址为0x162000,执行完毕第三条指令时eax寄存器的值即为0x162000:

    这就是Shellcode常用的重定位过程,下面继续跟踪时需要注意,执行到偏移0x1D61D函数时执行了两次call指令(内存地址0x16200e处call 0x16351F、0x163524处call 0x17F611,返回地址分别为0x162013与0x163529),因为call指令本身会将返回地址压栈,而程序执行过程中参数是通过栈传递的,所以这两次call 指令相当于将返回地址中指向Shellcode本身数据的指针当成了参数传递给了0x1D61D函数,单步执行到0x1D61D函数,内存中对应:

    根据函数调用约定可以看到在call 0x17F61D指令之前之前,ESP栈顶指向的内存值即分别为参数列表,参数3为上述提到的返回值0x163529。

    0x1D61D偏移处函数即可以使用IDA反编译,下面查看代码:

    开头这一部分就是Shellcode最常用的根据FS寄存器获取GetProcAddress函数地址的代码,格式基本都是固定的,下面的代码继续获取函数地址,直到开始解密Shellcode中的DLL数据:

    代码执行到下述位置为判断解密出来的数据是否是正确的DLL格式:

    直接在此位置下断,对应内存地址0x1801B4,文件偏移0x1E1B4(为了方便查看,IDA里rebase下基址):

    得到解密之后的完整DLL文件:

    右键DUMP内存到文件(文件大小0x2F000,其实可以填写的特别大,DUMP数据比DLL数据多不影响DLL运行及分析,且DLL所在内存空间本身只有0x2F000,实际最大只能DUMP这么大空间):

    后面的代码为解析DLL格式,将其分区段加载到内存:

    修正导入表:

    调用DLL,首先执行DllEntryPoint,再执行DLL导出函数Main(注意a1、a2等赋值可能成为DLL的Main函数运行参数。没有再继续跟进了):

    这段代码在IDA里对应:

一些技巧

IDA重定义基址:

    在IDA中为了方便对应内存中的地址,可以使用rebase功能将ida中代码基址重定义成0x162000:

    输入0x162000并点击确定:

样本下载:下载链接