Shellcode的原理与编写

简介

什么是shellcode

ShellCode是不依赖环境,放到任何地方都能够执行的机器码

编写ShellCode的方式有两种,分别是用编程语言编写或者用ShellCode生成器自动生成

ShellCode生成器生成的shellcode功能比较单一,常见的ShellCode生成器有shell storm、Msfvenom等

而用编程语言写的shellcode会更加突显灵活性,可以自己添加或修改功能

shellcode的原理

将shellcode注入缓冲区,然后欺骗目标程序执行它。而将shellcode注入缓冲区最常用的方法是利用目标系统上的缓冲区溢出漏洞。

环境搭建

1.修改程序入口点

为什么要修改程序入口点?VS新建一个32位的控制台程序,如下代码所示

int main()
{
    return 0;
}

然后把生成的程序拖入IDA中打开,在左边的函数窗口可以发现除了main函数以外还有一些其他函数,这是因为没有修改函数入口点所导致的

image-20220815153351967

在VS中的项目属性找到链接器的高级选项进行修改函数入口点

image-20220815153725968

在配置选项将Debug修改成Release, 解决方案配置也修改成Release

image-20220815155953993
image-20220815160003761

将重新生成的程序拖入IDA中, 可以发现少了很多的函数

image-20220820231802029

2.关闭缓冲区安全检查

项目属性的C++代码生成处禁用安全检查

若没禁用安全检查,那么ShellCode则无法执行

image-20220815160504476

3.设置项目兼容XP系统

在项目属性的C++代码生成处更改成MT(Release版本修改成MT,Debug版本则修改成MTD)

image-20220815161450164

4.关闭生成清单

image-20220815162116994

5.关闭生成调试信息

image-20220815162300047

6.启用最大优化

选择最大优化,这样生成的shellcode会小些

image-20240909181314348

探索PEB

什么是PEB

TEB:全称Thread Environment Block,中文名为线程环境块。系统在此TEB中保存频繁使用的线程相关的数据,一般存储在fs:[0]

PEB:全称Process Environment Block,中文名为进程环境块。存放进程信息,每个进程都有自己的PEB信息。通常存储在fs:[0x30]。这里主要通过PEB来获取kernel32.dll的基址

获取kernel32的基址

1.TEB->PEB

在TEB结构搜寻PEB,如下图所示TEB的结构代码, peb结构在30h的位置

在汇编里fs:[0]代表TEB的地址, 所以PEB的地址是fs:[30]

image-20220816200432109

2.PEB->LDR

如下代码是PEB结构, LDR处于0C位置

LDR: 包含加载模块的信息

3.LDR->InLoadOrderModuleList

LDR指向PEB\_LDR\_DATA结构, 要注意下图三个用蓝色标明的成员,分别是InMemoryOrderModuleListInLoadOrderModuleListInInitializationOrderModuleLists,这三者分别代表模块在不同状态下的排列顺序

也就是说这三个链表存放着所有模块,只是存放顺序不一样,这里选择跟进InLoadOrderModuleList

InLoadOrderModuleList:按模块加载的顺序

InMemoryOrderModuleList :按内存排列的顺序

InInitializationOrderModuleLists:模块初始化的装载顺序

img

4.遍历InLoadOrderModuleList链表

依照InLoadOrderModuleList的模块排列顺序,第一个模块是teb.exe,也就是可执行文件本身;第二个模块是ntdll.dll;第三个模块是kernel32.dll

不管是在XP系统还是win7及以上的系统,前三个模块的排列顺序都是不变的,所以只需找到链表的第三个位置就是kernel32.dll

5.获取模块基址

如下图所示,链表是指向LDR_DATA_TABLE_ENTRY结构的指针,只需关注三个成员DLLBaseFullDllNameBaseDllName,而DLLBase代表模块基址

DLLBase:模块的基址

FullDllName:包含路径的模块名

BaseDllName:不包含路径的模块名

img

编写汇编

从运行结果可以发现,通过peb获取的kernel模块基址和LoadLibrary函数获取的模块基址是一致的

image-20220816235755005

生成函数地址的规律

要先编写一个完整的ShellCode框架,首先要了解在VS中生成函数地址的规律,这样才能确定生成Shellcode的字节

单文件的函数生成规律

如下代码所示,A函数编写在B函数的前面,以这种形式编译的程序,生成的代码时A函数的地址会排在B函数的前面,也就是说在单文件里的函数生成规律与其编写代码时定义的函数排列位置有关

将生成的程序拖入IDA中查看可以发现,A函数的地址排在B函数前面

image-20220820194739481

多文件的函数生成规律

测试代码如下,分别有一个头文件header.h和三个cpp文件A.cpp、B.cpp、test.cpp

把生成的程序放到IDA查看,发现A函数的地址在B函数地址的前面,那么这个是由什么决定的呢

image-20220820200022472

多文件的函数生成规律与cpp文件生成代码的顺序有关

如下图所示,A.cpp排在B.cpp前面, 那么A.cpp里的函数就会排在B.cpp里的函数前面

若你把A.cpp的名字改成C.cpp, 那么B.cpp里的函数就会排在C.cpp里的函数前面, 也就是说cpp文件的第一个首字母或者数字会决定函数的生成规律, 字母按照AZ, 数字按照09

image-20220820200400126

当然除了更改cpp文件名字以外还有其他方法可以更改函数生成规律, 找到C程序项目所在目录, 打开后缀名为vcxproj的文件

image-20220820203117354

更改如下图所示的排列顺序也可以影响函数的生成规律

image-20220820203314220

ShellCode编写原则

1.不能使用常量字符串

只要出现了常量字符串,程序会把字符串存放在常量区段,所以要对字符串进行“打散”处理

2.不能直接调用系统函数

如下代码所示,直接调用了系统api函数的代码不能作为ShellCode

如下代码所示,可以通过GetProcAddressLoadLibraryA这两个函数实现动态调用系统api函数

但是这样不也是调用了系统函数吗,没关系,后续会讲到通过kernel32.dll模块来获取GetProcAddress函数地址,以此来实现动态调用所有的api函数

3.不能有全局变量

vs会将全局变量编译在其他区段中 结果就是一个绝对的地址, 也不能使用static来声明变量,因为其效果和全局变量一样

简单的ShellCode实例

image-20220820205555070

完整的ShellCode实例

ShellCode生成代码

header.h

api.h

0_Entry.cpp

1_ShellCodeStart.cpp

2_work.cpp

3_ShellCodeEnd.cpp

ShellCode加载代码

运行结果

执行ShellCode生成程序后生成了bin文件

image-20220820213225046

将bin文件拖入到ShellCode加载程序中, 成功执行ShellCode代码中的弹框

image-20220820213408449
image-20220820213443574

参考链接

  • https://www.bilibili.com/video/BV1y4411k7ch?p=3&vd_source=a6caf742912abf241ffbcb3c11933841

  • https://xz.aliyun.com/t/10478#toc-8

  • https://www.cnblogs.com/thresh/p/12609659.html

总结

1、编写ShellCode前需要先了解PE结构、汇编和win32编程相关的知识,这里推荐大家去看滴水的逆向课程,主讲老师是海东老师,BiliBili上能直接搜到

2、这里推荐一款用于查找api函数所在哪个dll的工具:DllExportFinder

image-20220820230829732

最后更新于