首页
社区
课程
招聘
[原创]分析实战读书笔记14_shellcode相关
发表于: 2021-3-29 13:25 5523

[原创]分析实战读书笔记14_shellcode相关

2021-3-29 13:25
5523

攻击者们为了绕过安全软件的检测并且加大分析师们的工作量,常常用各种手法保护隐藏病毒的核心代码。这其中就包含了一种利用shellcode的技术。在过去这种技术常用于漏洞利用中,但目前越来越多的样本也开始使用shellcode隐藏核心代码,同时也加大了分析师们的分析难度。了解shellcode的原理是一名病毒分析师必备的基础技能。这样在以后的工作中即使遇到这类样本也不会无从下手。

shellcode常指一段二进制数据,这段二进制数据独立存在,不像常规PE数据那样加载运行。它没有导入表导出表之类的结构,这使得shellcode极为灵活,同时也加大了它的编写难度。一段健壮的shellcode应具备很好的兼容性,在同一次攻击活动中应保证在不同的机器中都能无错执行。因此这使得一段shellcode代码永远离不开几个核心技术:

寻址纯偏移化 识别执行位置 手动搜寻API模拟导入表 空指令雪橇

一个PE文件拥有着健壮清晰的PE结构,这样的结构保证了程序的正常执行,其中如函数地址 全局变量等数据以绝对地址的形式存储在PE结构中。PE结构中的ImageBase字段指定了程序将要加载的起始地址。但并不是所有PE文件都可以如愿以偿的加载到ImageBase处,因此,PE结构中提供了一种叫做重定位表的数据结构,通过重定位表可以让程序在任意基质都能实现绝对地址的正确访问。

但shellcode仅是一段二进制数据,在被利用时无法指定自身代码的起始执行位置,这就导致了类似全局变量的绝对地址寻址在shellcode中变得不可靠。而全局变量这种形式的数据又避免不了要存在于shellcode代码中。

因此开发者们大开脑洞,将一段shellcode模拟为一个PE文件,将宿主进程模拟为操作系统。这样对于shellcode来讲,全局变量其实就变成了对于自身代码起始位置固定偏移处的一个内存地址,由于shellcode代码是编写者自己指定的,因此这种通过模拟ImageBase的方式在shellcode中成为了核心技术。

既然确定了寻址模式,那么就需要考虑如何获得shellcode的起始地址?这又引出了另一种技术,识别执行位置

当shellcode被加载并执行时,EIP所指向的应是整段shellcode的起始处,这样就保证了一点,shellcode的模拟ImageBase已经存在于EIP寄存器中了,下一步就变成了如何从EIP中取值

Intel并未提供任何对于指令指针寄存器EIP的直接访问指令。但有一些指令却可以通过组合来获得EIP中的值。这其中有两种思路:call popfnstencv

当call指令执行时,call后进阶的指令的起始地址会被压入堆栈以供retn使用。通过call pop组合可以取出下一个指令的地址,减去call指令的长度就可以得到call指令的起始地址,通过一个例子可以直观体现:

fstenv指令用于将保存了FPU寄存器信息的结构体保存到指定内存地址中。具体含义不用多考虑,我们只需要关注这个结构体:

image-20210325161418712

该结构体偏移0xC处的fpu_instruction_pointer保存着被FPU使用的最后一条指令(修改了FPU堆栈的指令)的地址。

配合fldz fld1等浮点运算指令一起使用可以为这个结构体赋值。如下面这个例子:

通过以上两种方式我们已经可以获取到了EIP,并将其存入EAX寄存器中,下面通过一个小例子感受下这种技术在shellcode中的神奇应用:

可以看到我们以EDX作为基址指针完成了一个简单的加法运算,期间还调用了一个call指令。call指令内部正确使用了全局变量。

我们将shellcode二进制数据放到IDA和OD上看看效果:

image-20210325165738538

image-20210325165801429

可以看到所有寻址都是正确的,后面我们会用一个小工具来调试我们这段shellcode。

这是一个最简单也是最直观的例子来演示shellcode是如何寻址的。

讲完了寻址我们还要继续思考一个问题:一个程序可能不用任何API函数而纯靠自身PE数据去完成功能执行吗?

在用户层,所有涉及与硬件、系统进行交互的操作,都必须依赖Windows提供的API函数。

如果一个用户层程序不包含任何API,那么它能做的也只能是疯狂的对自身进程内存进行读写,而无法走出自身进程去与外界交互。

所以在shellcode中使用API是必要的,但是没有了导入表,如何才能获取到自己想要的API地址呢。在正常开发中,我们使用LoadLibrary与GetProcAddress来动态加载Dll库获取想要的API函数。

但在shellcode中,即使这两个函数的地址我们也不知道。这就引出了Windows的强大设计:PEB。通过PEB结构我们可以通过内存搜索找到kernel32.dll的基地址,从而搜索LoadLibrary与GetProcAddress的地址。

在开始搜索前,我们需要简单了解下PEB。PEB是windows为开发者提供的一个包含了当前进程大量信息的结构体数据。通过PEB中的成员向下查找可以找到一个保存着进程内所有模块信息的结构体链表,通过遍历链表可以找到kernel32的基址。

如果想了解这种技术的原理可以看我上一篇文章:PEB结构:获取模块kernel32基址技术及原理分析

我们直接来看下简单的搜索kernel32.dll的shellcode代码:

得到了kernel32基址后,通过解析PE结构找到导出名称表和导出地址表和导出序号表,然后通过对比函数名来找到函数地址。

只要写过PE解析器的小伙伴做这个解析那不要太简单,这里就不赘述具体方法了。毕竟本章不是教你写shellcode,而是教你识别、分析shellcode

如果shellcode中引用了过多的导出函数,那么如果依次进行字符串比较,会占用大量的空间预先存储想要导入的函数名字符串。某些场景当shellcode数据大小被限制时,这种方法将无法满足需求。这时需要使用散列算法来进行比较。算法可以非常简单,只需要保证当前shellcode所使用的函数名的散列值不同即可。

例如:32位旋转向右累加散列算法。然后通过将预先计算好的散列值与实际计算出来的散列值比较来判断函数名。

由于使用散列算法节省了很多空间,并且还达到了隐藏字符串的目的,所以在恶意样本中很常见,因此必须要学会识别散列算法。只有知道了某段shellcode是在参与函数名比较才能继续向下分析。

当shellcode用于漏洞攻击中时,它必须使自身看起来像是一个合法的数据。从而通过函数内部的检测来实现漏洞的利用。

如:strcat和strcpy,其对于参数中的字符串并未进行长度限制,因此可以通过缓冲区溢出来执行shellcode。但由于其将参数试做字符串处理,因此当遇到00字节时,会解析为字符串结尾。因此在shellcode中无法出现00数据。

image-20210329093618962

为了克服这一问题,编写者们通常采用解码器的方式来运行shellcode。只需将解码器的代码进行仔细编写即可。而核心代码则通过解码器解码后执行。

空指令雪橇不是必需的,它会增加shellcode成功运行的几率。

image-20210329093826645

通过向shellcode前插入大段nop指令来实现,这样只要漏洞的跳转地址命中了雪橇中的任意一个nop代码。都会像雪橇一样一直向后滑行,直至命中真正的shellcode代码开始执行。

在PE文件中寻找shellcode是相对简单的,因为程序会使用混淆技术编写。或是将shellcode注入另一个进程内执行。

因此可以通过查找API调用来定位shellcode :

VirtualAllocEx WriteProcessMemory CreateRemoteThread

或是通过硬编码方式识别shellcode解码器:

image-20210329094449246

其他格式文件中也可能会包含shellcode,如Adobe Reader的CVE-2010-0188漏洞,则允许PDF文件中包含shellcode并运行。

本文主要记录下shellcode中一些核心技术的原理和基础写法,这样在日后分析样本涉及shellcode时不会不知所措。

这段shellcode是如何编码的?

image-20210329095834611

这段shellcode手动导入了哪个函数?

image-20210329101346107

这段shellcode和哪个网络主机通信?

image-20210329101430443

这段shellcode在文件系统上留下了什么迹象?

这段shellcode做了什么?

image-20210329101521420

这段shellcode被注入到什么进程中?

image-20210329105220146

这段shellcode位于哪里?

image-20210329105314683

这段shellcode是如何被编码的?

image-20210329105452242

这段shellcode手动导入了哪个函数?

image-20210329110248864

这段shellcode和什么网络主机进行通信?

image-20210329112027179

这段shellcode做了什么?

image-20210329112425557

这个PDF中使用了什么漏洞?

image-20210329112723865

这段shellcode是如何编码的?

这段shellcode手动导入了哪个函数?

image-20210329113543028

这段shellcode在文件系统上留下了什么迹象?

image-20210329114446765

image-20210329114556794

这段shellcode做了什么?

 
 
 
 
0x4000  E8 00 00 00 00     call 0x4005   //call目标地址 = 起始地址+偏移+指令长度(5)  偏移设置为0即可call下一行
0x4005  58                 pop eax       //pop eax可以将4005存入eax寄存器
0x4006  83 E8 05           sub eax,5     //sub eax,5  将eax减5后 eax执行4000  整段代码起始地址
0x4000  E8 00 00 00 00     call 0x4005   //call目标地址 = 起始地址+偏移+指令长度(5)  偏移设置为0即可call下一行
0x4005  58                 pop eax       //pop eax可以将4005存入eax寄存器
0x4006  83 E8 05           sub eax,5     //sub eax,5  将eax减5后 eax执行4000  整段代码起始地址
 
 
 
0x4000    D9 EE        fldz            //将浮点数0压入FPU堆栈,结构体成员fpu_instruction_pointer被赋值为0x4000
0x4002    83 EC 20    sub esp,0x20    //提升栈顶,为结构体开辟空间
0x4005    D9 74 24 F4    fstenv [esp-0xC]    //将结构体从esp-C处开始赋值,则ESP处刚好为fpu_instruction_pointer成员
0x4009    58            pop eax        //取出fpu_instruction_pointer成员至eax,此时eax为0x4000,记得恢复堆栈
0x4000    D9 EE        fldz            //将浮点数0压入FPU堆栈,结构体成员fpu_instruction_pointer被赋值为0x4000
0x4002    83 EC 20    sub esp,0x20    //提升栈顶,为结构体开辟空间
0x4005    D9 74 24 F4    fstenv [esp-0xC]    //将结构体从esp-C处开始赋值,则ESP处刚好为fpu_instruction_pointer成员
0x4009    58            pop eax        //取出fpu_instruction_pointer成员至eax,此时eax为0x4000,记得恢复堆栈
//该shellcode计算1+2的值,所用变量全部模拟全局变量
 
 
0x4000  E8 00 00 00 00                         call 0x4005   //0x4005压入堆栈
0x4005  5A                                     pop edx       //4005存入edx寄存器
0x4006  83 EA 05                               sub edx,5     //将shellcode基址存入edx
0x4009  C7 82 22 00 00 00 01 00 00 00        mov dword ptr ds:[edx+0x22],1    //为全局变量赋值
0x4013  C7 82 26 00 00 00 02 00 00 00        mov dword ptr ds:[edx+0x26],2    //为全局变量赋值
0X401D  E8 10 00 00 00                        call 0x4035        //调用函数,实现加法
 
0x4022  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00        //存放全局变量所用数据区
 
0x4032  8B 42 22                            mov eax,dword ptr ds:[edx+0x22]      //加法汇编
0x4035  03 42 26                            add eax,dword ptr ds:[edx+0x26]
0x4038  89 42 2A                            mov dword ptr ds:[edx+0x2A],eax      //将结果存入第三个全局变量
0x403B    C3                                    retn
//该shellcode计算1+2的值,所用变量全部模拟全局变量
 
 
0x4000  E8 00 00 00 00                         call 0x4005   //0x4005压入堆栈
0x4005  5A                                     pop edx       //4005存入edx寄存器
0x4006  83 EA 05                               sub edx,5     //将shellcode基址存入edx
0x4009  C7 82 22 00 00 00 01 00 00 00        mov dword ptr ds:[edx+0x22],1    //为全局变量赋值
0x4013  C7 82 26 00 00 00 02 00 00 00        mov dword ptr ds:[edx+0x26],2    //为全局变量赋值
0X401D  E8 10 00 00 00                        call 0x4035        //调用函数,实现加法
 
0x4022  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00        //存放全局变量所用数据区
 
0x4032  8B 42 22                            mov eax,dword ptr ds:[edx+0x22]      //加法汇编
0x4035  03 42 26                            add eax,dword ptr ds:[edx+0x26]
0x4038  89 42 2A                            mov dword ptr ds:[edx+0x2A],eax      //将结果存入第三个全局变量
0x403B    C3                                    retn
 
E8 00 00 00 00 5A 83 EA 05 C7 82 22 00 00 00 01 00 00 00 C7 82 26 00 00 00 02 00 00 00 E8 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8B 42 22 03 42 26 89 42 2A C3
E8 00 00 00 00 5A 83 EA 05 C7 82 22 00 00 00 01 00 00 00 C7 82 26 00 00 00 02 00 00 00 E8 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8B 42 22 03 42 26 89 42 2A C3
 
 
 
 
 
 
 
 
 
 
push esi                //保存esi寄存器
xor eax,eax                //清空eax
mov eax,fs:[0x30]        //取PEB结构首地址
mov eax,[eax+C]            //取PEB结构中的Ldr成员(_PEB_LDR_DATA结构首地址)
mov esi,[eax+1C]        //取_PEB_LDR_DATA -> InInitializationOrderModuleList -> Flink   找到_LDR_DATA_TABLE_ENTRY节点中的Flink
mov eax,[esi]            //找到第二个_LDR_DATA_TABLE_ENTRY节点中的Flink
mov eax,[eax+8]            //取出0x18处的DllBase成员
pop esi                    //恢复ESI
retn                    //退出
//本例假设第二个节点为kernel32.dll    在旧系统中第二个固定为kernel32.dll  目前已经不固定,需要加dllname的判断
push esi                //保存esi寄存器
xor eax,eax                //清空eax
mov eax,fs:[0x30]        //取PEB结构首地址
mov eax,[eax+C]            //取PEB结构中的Ldr成员(_PEB_LDR_DATA结构首地址)
mov esi,[eax+1C]        //取_PEB_LDR_DATA -> InInitializationOrderModuleList -> Flink   找到_LDR_DATA_TABLE_ENTRY节点中的Flink
mov eax,[esi]            //找到第二个_LDR_DATA_TABLE_ENTRY节点中的Flink
mov eax,[eax+8]            //取出0x18处的DllBase成员
pop esi                    //恢复ESI
retn                    //退出
//本例假设第二个节点为kernel32.dll    在旧系统中第二个固定为kernel32.dll  目前已经不固定,需要加dllname的判断
 
 
 
HashString:        //计算字符串hash
    push esi
    push edi
    mov esi,[esp+0xC]
calc_hash:
    xor edi,edi
    cld
hash_iter:
    xor eax,eax
    lodsb
    cmp al,ah
    je hash_done
    ror edi,0xD
    add edi,eax
    jmp hash_iter
hash_done:
    mov eax,edi
    pop edi
    pop esi
    retn 4
HashString:        //计算字符串hash
    push esi
    push edi
    mov esi,[esp+0xC]
calc_hash:
    xor edi,edi
    cld
hash_iter:
    xor eax,eax
    lodsb
    cmp al,ah
    je hash_done
    ror edi,0xD
    add edi,eax
    jmp hash_iter
hash_done:
    mov eax,edi
    pop edi
    pop esi
    retn 4

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 3
支持
分享
最新回复 (3)
雪    币: 699
活跃值: (103)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
2021-3-30 00:58
0
雪    币: 177
活跃值: (79)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
总结的很好,谢谢
2021-4-2 08:34
0
雪    币: 1475
活跃值: (14652)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
4
hemp 总结的很好,谢谢
一起学习
2021-4-2 08:54
0
游客
登录 | 注册 方可回帖
返回
//