上一集主要说的是杀软如何的检测,以及大致的实现方法,重点是诸多的检测指标,做了一些理论基础的准备
这一集来动手搓一个实际的样本出来喵
鄙人技术浅陋,经网上四处观看浏览总结得此文,如有不足希望师傅们多包容⊙﹏⊙
下面正文开始
准备 – 一只 Shellcode 被赋予的使命
shellcode 的生成在上一集做过了,也没什么可讲的,用现成的 c2 鼠标点点点就行,msf,cs 啥的都能用
生成的 shellcode – 看到的那一大堆的字符其实是有实际代表的功能滴,但是涉及的东西太多,太底层,这里先跳过喵,之后有机会再单独开一集讲讲(
但是如上一集介绍 shellcode 的时候所说的一样,这个东西生成出来就是一串字,写进数组里面 emmm,然后是怎么到创建连接远程上线的呢,下面说一说
shellcode 要想实现诸如创建 TCP 连接,读写文件,执行命令甚至调用工具等功能,都必须要依靠 Windows 系统本身提供的 API 函数进行操作
//说完这句话自然会引出一个问题,为啥是必须这么干啊,这个问题后续会说的喵
而那些自带的关键 API 函数(比如 GetProcAddress ,或者这个系列第一集提到过的 LoadLibrary 函数等)都存放在系统的 dll 里面,比如 kernel32.dll、kernelbase.dll、ntdll.dll 之类的,shellcode 里面是不能写死这些函数的地址的,因为不同的 win 版本,不同补丁环境等等非常繁多的因素影响下,这些函数在内存里面的地址都会不一样,因此 shellcode 必须要先主动的找到这些函数的地址,主要通过 TEB 和 PEB 来实现
TEB(线程环境块)
每个线程在对应的内存中都有一个专属的 TEB 结构体,里边存储了当前线程的信息,比如线程 ID、栈的起始地址之类的东西,但是最关键的是其中存有指向 PEB 的指针
TEB 结构体里有一个字段叫 ProcessEnvironmentBlock,它的作用就是 指向当前进程的 PEB,这个字段在 TEB 里的偏移也是固定的(x86、x64 都是 0x00)只要能找到 TEB 就一定能获得这个指针
那 TEB 在需要用的时候咋找到呢?在 x86 架构,TEB 的地址可以通过寄存器 fs 直接获取,固定偏移 0x30,在 x64 架构下,通过寄存器 gs 获取,固定偏移是 0x60,这个偏移是 Win 系统的 “硬规则”,不会变化的喵
然后通过找到里面的指针抓住 PEB
PEB(进程环境块)
PEB 和 TEB 就好像那个海尔兄弟,舒克和贝塔,都差不多,每个进程在内存里面也都有一个 PEB 结构体,里面也是存着一堆像是进程 ID 之类的东西,最关键的内容是其中有已经加载出来的 DLL 的列表
上文说过,要想找到关键函数地址,这个地址在哪都是不一样的,需要找到对应所属的 dll,那通过 TEB 找到 PEB 之后就可以获取到 dll 加载在哪了
PEB 里边有一个 LDR 字段,指向一个结构体 PEB_LDR_DATA ,其中就存储有一个已加载出来的 dll 的链表,启动进程时,会把所有默认加载的 DLL(比如我们要找的 kernel32.dll)按顺序存在这个链表中
链表的每一个节点都存有三个数据:DLL 的名字(比如 kernel32.dll)、DLL 在内存中的起始地址(基地址)、指向下一个节点的指针
至此答案已经显而易见了喵!
只要遍历这个链表,逐个对比 DLL 名字,直到找到需要的 kernel32.dll(或 kernelbase.dll)—— 因为 LoadLibrary 和 GetProcAddress 这两个函数就在这个 DLL 里,拿到 kernel32.dll 的起始地址后,就可以通过这个 DLL 找到 LoadLibrary 和 GetProcAddress 的具体地址,这个目的是通过 解析 dll 的 PE 结构 来实现的:
- 首先每个 DLL 都是一个 PE 文件(Windows 可执行文件格式),PE 结构里有一个导出表(Export Table)
- 导出表会列出这个 DLL 对外暴露的所有函数的名字和对应的内存偏移量
- 解析导出表,找到 LoadLibrary 和 GetProcAddress 这两个函数对应的偏移,再加上 dll 的基地址,就能得出函数的实际内存地址
拿到这两个函数的地址后,就可以用 LoadLibrary 加载其他的 dll,用 GetProcAddress 查找 dll 里的具体函数(比如 socket、connect,拿来创建 TCP 连接)
终于… shellcode 如愿以偿的可以获得想要的函数,通过这些函数去完成一个 c2 的使命
利用已知的平台(windows)特定机制来执行特定操作,因此有时在一些地方会称呼 shellcode 为引导代码
将 shellcode 代入执行
回到最开始那个问题,shellcode 做的事情已经说明白了,那他是怎么从一个字符串数组开始做那么多的捏
这就说到 C++ 了,执行字符数组的经典写法是将其作为函数指针:
void (*func)();
func = (void (*)()) code;
func();
更简短浓缩一点的单行写法:
(*(void(*)()) code)();
这里就可以回答之前的问题了喵,为什么要使用 shellcode ,导致必须调用系统的 API 函数来实现功能,既然已经用 c++ 写 loader 了,为啥不干脆用 c++ 实现那些功能?
显而易见喵,shellcode 可以在生成之前,通过 c2 平台直接一下子生成出来,还可以选择很多自定义的配置,不用重复改动 C++ 代码,而且直接拿 C++ 写恶意代码去做 c2 太容易被杀软识别,而 Shellcode 的操作空间可就多了喵(其实方便才是最关键的hh
但是话又说回来,有这么两个问题:
- windows 系统的数据执行保护机制,特别是栈上的数据受到执行保护(DEP)
- 要给 Shellcode 分配可执行的内存空间,并启动线程执行
实际执行 shellcode 的方法有点区别:
首先,使用 VirtualAlloc(或用于远程进程的 VirtualAllocEx)这俩都是 API 函数,可以通过上文说的 TEB/PEB 找到,因为数组存在栈上,而 win 不允许栈上数据进行执行,就是防一手恶意代码注入内存,用 VirtualAlloc 就可以主动申请一块 “可执行” 权限的内存(PAGE_EXECUTE_READWRITE),绕开 DEP 的限制
然后把 shellcode 填充进去(用 RtlCopyMemory 函数)然后分别使用 CreateThread 或 CreateRemoteThread 函数创建新线程
#include <Windows.h> //包含 Windows API 头文件
void main(){
//准备好Shellcode数组
const char shellcode[] = " ";
//分配可执行的内存空间
PVOID shellcode_exec = VirtualAlloc(
0, // 让系统自动分配内存地址
sizeof shellcode, // 内存大小=Shellcode 的长度
MEM_COMMIT|MEM_RESERVE, // 申请内存并提交(必须参数)
PAGE_EXECUTE_READWRITE // 内存权限rwx
);
//把 Shellcode 复制到分配好的内存中
RtlCopyMemory(shellcode_exec, shellcode, sizeof shellcode);
//RtlCopyMemory复制数据
//创建新线程,执行内存中的 Shellcode
DWORD threadID; //存储线程ID
HANDLE hThread = CreateThread(
NULL, //无安全属性
0, //线程栈大小默认
(PTHREAD_START_ROUTINE)shellcode_exec, //线程执行的代码地址(就是 Shellcode 所在内存)
NULL, //给线程传的参数,暂时设为null
0, //线程创建后立即执行
&threadID //接收线程 ID 的变量
);
//等待线程执行完成
WaitForSingleObject(hThread, INFINITE);
}
都打了注释,可以看看喵
解释一下为什么要创建新线程,直接执行 shellcode_exec 也可以,但用 CreateThread 能让 Shellcode 在后台运行,程序主线程不会被阻塞,更隐蔽一些
单纯的写出来这么一个很原生的 loader,来测一测免杀效果试试?直接上 vt

19/72,还不错喵,但是还是会被 defender 等主流强力杀软干掉的,下一集搞一搞进阶一点的免杀手法,争取把检出率再压低一点
