import binascii
import hashlib
from LyScript32 import MyDebug
def crc32(data):
return "0x{:X}".format(binascii.crc32(data) & 0xffffffff)
def md5(data):
md5 = hashlib.md5(data)
return md5.hexdigest()
if __name__ == "__main__":
dbg = MyDebug()
dbg.connect()
# 循环节
section = dbg.get_section()
for index in section:
# 定义字节数组
mem_byte = bytearray()
address = index.get("addr")
section_name = index.get("name")
section_size = index.get("size")
# 读出节内的所有数据
for item in range(0,int(section_size)):
mem_byte.append( dbg.read_memory_byte(address + item))
# 开始计算特征码
md5_sum = md5(mem_byte)
crc32_sum = crc32(mem_byte)
print("[*] 节名: {:10s} | 节长度: {:10d} | MD5特征: {} | CRC32特征: {}"
.format(section_name,section_size,md5_sum,crc32_sum))
dbg.close()
运行后等待片刻,读者应该可以看到如下图所示的输出结果,图中每一个节的散列值都被计算出来;
4.2.3 验证PE启用的保护模式
PE文件的保护模式包括了、随机基址(Address space layout randomization,ASLR)、数据不可执行(Data Execution Prevention,DEP)、强制完整性(Forced Integrity,FCI)这四种安全保护机制,它们的主要作用是防止恶意软件攻击,并提高系统的安全性和可靠性。
1.随机基址(Address space layout randomization,ASLR)
随机基址是一种Windows操作系统中的内存防护机制,它可以使恶意软件难以通过内存地址预测来攻击应用程序。在随机基址机制下,操作系统会随机改变应用程序的地址空间布局,使得每次运行时程序在内存中加载的地址不同,从而防止攻击者凭借对程序内存地址的猜测或破解来攻击程序。
随机基址的验证方式是定位到PE结构的pe.OPTIONAL_HEADER.DllCharacteristics
并对PE文件的OPTIONAL_HEADER
中的DllCharacteristics
属性进行取值位运算操作并以此作为判断的依据:
首先,使用hex()
将pe.OPTIONAL_HEADER.DllCharacteristics
转换为16进制字符串。
然后,将该16进制字符串和0x40
进行按位与运算。
最后,将得到的结果与0x40
进行比较。
根据微软的文档,pe.OPTIONAL_HEADER.DllCharacteristics
是一个32位的掩码属性,用于描述PE文件的一些特性。其中DllCharacteristics
的第7位(从0开始)表示该文件是否启用了ASLR(Address Space Layout Randomization)
特性,如果启用,则对应值为0x40。因此,上述代码的作用是判断该PE文件是否启用了ASLR
特性。如果结果为真,则说明该文件启用了ASLR
;否则,说明该文件未启用ASLR。
数据不可执行(Data Execution Prevention,DEP)
数据不可执行是一种Windows操作系统中的内存防护机制,它可以防止恶意软件针对系统内存中的数据进行攻击。在DEP机制下,操作系统会将内存分为可执行和不可执行两部分,其中不可执行部分主要用于存放程序数据,而可执行部分用于存放代码。这样当程序试图执行不可执行的数据时,操作系统会立即终止程序,从而防止攻击者通过操纵程序数据来攻击系统。
同样使用位运算符&,对PE文件的OPTIONAL_HEADER
中的DllCharacteristics
属性进行了取值并进行了位运算操作。该代码的具体意义为:
首先,使用hex()将pe.OPTIONAL_HEADER.DllCharacteristics
转换为16进制字符串。
然后,将该16进制字符串和0x100
进行按位与运算。
最后,将得到的结果与0x100
进行比较。
根据微软的文档,pe.OPTIONAL_HEADER.DllCharacteristics
是一个32位的掩码属性,用于描述PE文件的一些特性。其中,DllCharacteristics
的第8位(从0开始)表示该文件是否启用了NX特性(No eXecute)
,如果启用,则对应值为0x100
。NX特性是一种内存保护机制,可以防止恶意代码通过将数据区域当作代码区域来执行代码,提高了系统的安全性。因此,上述代码的作用是判断该PE文件是否启用了NX特性。如果结果为真,则说明该文件启用了NX特性;否则,说明该文件未启用NX特性。
强制完整性(Forced Integrity,FCI)
强制完整性是一种Windows操作系统中的强制措施,它可以防止恶意软件通过DLL注入来攻击系统。在FCI机制下,操作系统会通过数字签名和其他校验措施对系统DLL和其他关键文件进行验证,确保这些文件没有被修改或替换。如果检测到文件已被修改或替换,操作系统将会拒绝这些文件并终止相关进程,这样可以保护系统的完整性和安全性。
同样使用位运算符&,对PE文件的OPTIONAL_HEADER
中的DllCharacteristics
属性进行了取值并进行了位运算操作。该代码的具体意义为:
首先,使用hex()将pe.OPTIONAL_HEADER.DllCharacteristics
转换为16进制字符串。
然后,将该16进制字符串和0x80进行按位与运算。
最后,将得到的结果与0x80进行比较。
根据微软的文档,pe.OPTIONAL_HEADER.DllCharacteristics
是一个32位的掩码属性,用于描述PE文件的一些特性。其中,DllCharacteristics
的第7位(从0开始)表示该文件是否启用了动态基址特性(Dynamic Base)
,如果启用,则对应值为0x40
。动态基址特性与ASLR(Address Space Layout Randomization)功能是紧密相关的,当启用ASLR时,动态基址特性也会被自动启用。因此,上述代码的作用是判断该PE文件是否启用了动态基址特性。如果结果为真,则说明该文件启用了动态基址特性;否则,说明该文件未启用动态基址特性。
根据如上描述,要想实现检查进程内所有模块的保护方式,则首先要通过dbg.get_all_module()
获取到进程的所有模块信息,当模块信息被读入后,通过dbg.read_memory_byte()
获取到该内存的机器码,并通过pefile.PE(data=byte_array)
装载到内存,通过对不同数值与与运算即可判定是否开启了保护。
随机基址 hex(pe.OPTIONAL_HEADER.DllCharacteristics) & 0x40 == 0x40
数据不可执行 hex(pe.OPTIONAL_HEADER.DllCharacteristics) & 0x100 == 0x100
强制完整性 hex(pe.OPTIONAL_HEADER.DllCharacteristics) & 0x80 == 0x80
那么根据如上描述,这段核心代码可以总结为如下案例;
from LyScript32 import MyDebug
import pefile
if __name__ == "__main__":
# 初始化
dbg = MyDebug()
dbg.connect()
# 得到所有加载过的模块
module_list = dbg.get_all_module()
print("-" * 100)
print("模块名 \t\t\t 基址随机化 \t\t DEP保护 \t\t 强制完整性 \t\t SEH异常保护 \t\t")
print("-" * 100)
for module_index in module_list:
print("{:15}\t\t".format(module_index.get("name")),end="")
# 依次读入程序所载入的模块
byte_array = bytearray()
for index in range(0, 4096):
read_byte = dbg.read_memory_byte(module_index.get("base") + index)
byte_array.append(read_byte)
oPE = pefile.PE(data=byte_array)
# 随机基址 => hex(pe.OPTIONAL_HEADER.DllCharacteristics) & 0x40 == 0x40
if ((oPE.OPTIONAL_HEADER.DllCharacteristics & 64) == 64):
print("True\t\t\t",end="")
else:
print("False\t\t\t",end="")
# 数据不可执行 DEP => hex(pe.OPTIONAL_HEADER.DllCharacteristics) & 0x100 == 0x100
if ((oPE.OPTIONAL_HEADER.DllCharacteristics & 256) == 256):
print("True\t\t\t",end="")
else:
print("False\t\t\t",end="")
# 强制完整性=> hex(pe.OPTIONAL_HEADER.DllCharacteristics) & 0x80 == 0x80
if ((oPE.OPTIONAL_HEADER.DllCharacteristics & 128) == 128):
print("True\t\t\t",end="")
else:
print("False\t\t\t",end="")
if ((oPE.OPTIONAL_HEADER.DllCharacteristics & 1024) == 1024):
print("True\t\t\t",end="")
else:
print("False\t\t\t",end="")
print()
dbg.close()
读者可以运行这段案例,即可看到如下图所示的输出效果;
4.2.4 PE结构的FOA/VA/RAV转换
在PE文件结构中,VA、RVA和FOA都是用来描述内存中数据的位置和在文件中的偏移量,具体含义如下:
VA(Virtual Address):虚拟地址,也叫映像地址,是指在内存中的地址。VA通常是程序运行时要访问的地址,由操作系统进行地址转换映射到物理地址上。
RVA(Relative Virtual Address):相对虚拟地址,是指当前位置相对于所在塞段的起始地址的偏移量。RVA通常是用于描述PE文件中的各个段的相对位置,它不像VA一样是用于真正运行程序的,而是在加载 PE 文件时进行重定位所使用的。
FOA(File Offset Address):文件偏移量,是指在文件中的偏移量,也就是从文件起始位置到数据的偏移量。FOA通常是用于描述PE文件中的各个段和头信息在文件中的位置,可以用来定位和修改文件中的数据。
需要注意的是,这三种地址是不同的,其值也不同。VA和RVA通常是在Windows操作系统中使用;FOA通常是在PE文件处理时使用。在PE文件加载时,Windows操作系统会将RVA转换为VA,将程序的段加载到内存中,并根据需要对其进行重定位(如果代码中包含有绝对地址的话),然后将控制权交给程序的入口点,程序进入执行状态。
首先实现VA转为FOA
的案例,在这段核心代码中,通过dbg.get_base_from_address(dbg.get_local_base())
获取到内存中的程序基地址,并与VA地址相减得到内存中的RVA地址,并调用PEfile库中的get_offset_from_rva
完成转换。
def get_offset_from_va(pe_ptr, va_address):
# 得到内存中的程序基地址
memory_image_base = dbg.get_base_from_address(dbg.get_local_base())
# 与VA地址相减得到内存中的RVA地址
memory_local_rva = va_address - memory_image_base
# 根据RVA得到文件内的FOA偏移地址
foa = pe_ptr.get_offset_from_rva(memory_local_rva)
return foa
其次是将FOA文件偏移转为VA
虚拟地址,此类代码与上方代码基本一致,通过pe_ptr.get_rva_from_offset
先得到RVA相对偏移,然后在通过dbg.get_base_from_address(dbg.get_local_base())
得到内存中程序基地址,然后计算VA地址,最后直接计算得到VA地址。
def get_va_from_foa(pe_ptr, foa_address):
# 先得到RVA相对偏移
rva = pe_ptr.get_rva_from_offset(foa_address)
# 得到内存中程序基地址,然后计算VA地址
memory_image_base = dbg.get_base_from_address(dbg.get_local_base())
va = memory_image_base + rva
return va
最后一种则是将FOA文件偏移地址转为RVA
相对地址,此类代码中通过枚举所有节中的参数,并以此动态计算出实际的RVA地址返回。
# 传入一个FOA文件地址转为RVA地址
def get_rva_from_foa(pe_ptr, foa_address):
sections = [s for s in pe_ptr.sections if s.contains_offset(foa_address)]
if sections:
section = sections[0]
return (foa_address - section.PointerToRawData) + section.VirtualAddress
else:
return 0
最终将三段代码整合在一起,即可构成一个互相转换的案例,至于PE结构的解析问题,详细度过PE结构篇的你不需要我做太多的解释了。
from LyScript32 import MyDebug
import pefile
# 传入一个VA值获取到FOA文件地址
def get_offset_from_va(pe_ptr, va_address):
# 得到内存中的程序基地址
memory_image_base = dbg.get_base_from_address(dbg.get_local_base())
# 与VA地址相减得到内存中的RVA地址
memory_local_rva = va_address - memory_image_base
# 根据RVA得到文件内的FOA偏移地址
foa = pe_ptr.get_offset_from_rva(memory_local_rva)
return foa
# 传入一个FOA文件地址得到VA虚拟地址
def get_va_from_foa(pe_ptr, foa_address):
# 先得到RVA相对偏移
rva = pe_ptr.get_rva_from_offset(foa_address)
# 得到内存中程序基地址,然后计算VA地址
memory_image_base = dbg.get_base_from_address(dbg.get_local_base())
va = memory_image_base + rva
return va
# 传入一个FOA文件地址转为RVA地址
def get_rva_from_foa(pe_ptr, foa_address):
sections = [s for s in pe_ptr.sections if s.contains_offset(foa_address)]
if sections:
section = sections[0]
return (foa_address - section.PointerToRawData) + section.VirtualAddress
else:
return 0
if __name__ == "__main__":
dbg = MyDebug()
dbg.connect()
# 载入文件PE
pe = pefile.PE(name=dbg.get_local_module_path())
# 读取文件中的地址
rva = pe.OPTIONAL_HEADER.AddressOfEntryPoint
va = pe.OPTIONAL_HEADER.ImageBase + pe.OPTIONAL_HEADER.AddressOfEntryPoint
foa = pe.get_offset_from_rva(pe.OPTIONAL_HEADER.AddressOfEntryPoint)
print("文件VA地址: {} 文件FOA地址: {} 从文件获取RVA地址: {}".format(hex(va), foa, hex(rva)))
# 将VA虚拟地址转为FOA文件偏移
eip = dbg.get_register("eip")
foa = get_offset_from_va(pe, eip)
print("虚拟地址: 0x{:x} 对应文件偏移: {}".format(eip, foa))
# 将FOA文件偏移转为VA虚拟地址
va = get_va_from_foa(pe, foa)
print("文件地址: {} 对应虚拟地址: 0x{:x}".format(foa, va))
# 将FOA文件偏移地址转为RVA相对地址
rva = get_rva_from_foa(pe, foa)
print("文件地址: {} 对应的RVA相对地址: 0x{:x}".format(foa, rva))
dbg.close()
如上代码中所传递地址必须保证是正确的,否则会报错,读者应根据自己的需求选择对应的函数来执行,代码运行后输出效果如下图所示;
4.2.5 PE结构检索SafeSEH内存地址
SafeSEH(Safe Structured Exception Handling)是Windows操作系统提供的一种安全保护机制,用于防止恶意软件利用缓冲区溢出漏洞来攻击应用程序。
当应用程序使用结构化异常处理机制(SEH)时,其异常处理链表(ExceptionHandler链表)可以被攻击者用来执行代码注入攻击。这是由于异常处理链表本质上是一个指针数组,如果应用程序使用了未经验证的指针指向异常处理函数,则攻击者可以构造恶意的异常处理模块来覆盖原有的处理程序,从而迫使程序执行攻击者注入的代码。这种攻击技术被称为SEH注入(SEH Overwrite)。
为了解决这个问题,SafeSEH机制被引入到Windows操作系统中。其主要思想是在应用程序的导入表中加入了一个SafeSEH表,用于存储由编译器生成的SEH处理函数的地址列表。在程序执行时,Windows操作系统将检查程序SEH链表中的指针是否存在SafeSEH表中。如果该指针不存在于SafeSEH表中,则Windows操作系统将终止应用程序的执行。SafeSEH机制可以提高系统的安全性和可靠性,防止恶意软件利用缓冲区溢出和SEH注入等漏洞来攻击应用程序。
SafeSEH的检索问题,读者可依据如下步骤依次实现;
1.初始化调试器dbg,并执行dbg.connect()连接到正在运行的进程。
2.memory_image_base = dbg.get_base_from_address(dbg.get_local_base()):获取程序内存镜像的基地址。
3.peoffset = dbg.read_memory_dword(memory_image_base + int(0x3c)):通过PE头的偏移地址得到PE头的基地址。
4.flags = dbg.read_memory_word(pebase + int(0x5e)):读取PE头中的DllCharacteristics属性的值,使用了read_memory_word函数,该值用于判断是否开启了SafeSEH保护。
5.如果flags值的第10位(即0x400)为1,则说明该程序已开启了SafeSEH保护,并输出保护状态为“NoHandler”;否则不打印状态信息。
6.numberofentries = dbg.read_memory_dword(pebase + int(0x74)):读取PE头中的NumberOfRvaAndSizes属性的值,该值用来描述数据目录表中的项目个数。
7.如果numberofentries值大于10,则说明该PE文件结构异常,退出程序。
8.sectionaddress和sectionsize用于指定程序头表中第10个PE节的地址和大小。如果第10个节大小不为0且为0x40或与第一个DWORD和第二个DWORD的值相等,则说明该程序在节表中找到了SafeSEH记录。
9.sehlistaddress和sehlistsize指定SafeSEH
记录列表的地址和大小。如果sehlistaddress
不为0且sehlistsize
不为0,则说明该程序启用了SafeSEH
保护,并输出保护状态。
10.如果condition等于False,则说明PE结构不符合要求或未启用SafeSEH保护。如果data小于0x48,则说明该DLL或EXE程序无法被识别。如果sehlistaddress和sehlistsize不同时等于零,则打印SafeSEH保护中的长度。
查找SafeSEH
内存地址,读入PE文件到内存,验证该程序的SEH保护是否开启,如果开启则尝试输出SEH内存地址,其实现代码可总结为如下案例;
from LyScript32 import MyDebug
import struct
LOG_HANDLERS = True
if __name__ == "__main__":
dbg = MyDebug()
dbg.connect()
# 得到PE头部基地址
memory_image_base = dbg.get_base_from_address(dbg.get_local_base())
peoffset = dbg.read_memory_dword(memory_image_base + int(0x3c))
pebase = memory_image_base + peoffset
flags = dbg.read_memory_word(pebase + int(0x5e))
if(flags & int(0x400)) != 0:
print("SafeSEH | NoHandler")
numberofentries = dbg.read_memory_dword(pebase + int(0x74))
if numberofentries > 10:
# 读取 pebase+int(0x78)+8*10 | pebase+int(0x78)+8*10+4 读取八字节,分成两部分读取
sectionaddress, sectionsize = [dbg.read_memory_dword(pebase+int(0x78)+8*10),
dbg.read_memory_dword(pebase+int(0x78)+8*10 + 4)
sectionaddress += memory_image_base
data = dbg.read_memory_dword(sectionaddress)
condition = (sectionsize != 0) and ((sectionsize == int(0x40)) or (sectionsize == data))
if condition == False:
print("[-] SafeSEH 无保护")
if data < int(0x48):
print("[-] 无法识别的DLL/EXE程序")
sehlistaddress, sehlistsize = [dbg.read_memory_dword(sectionaddress+int(0x40)),
dbg.read_memory_dword(sectionaddress+int(0x40) + 4)
if sehlistaddress != 0 and sehlistsize != 0:
print("[+] SafeSEH 保护中 | 长度: {}".format(sehlistsize))
if LOG_HANDLERS == True:
for i in range(sehlistsize):
sehaddress = dbg.read_memory_dword(sehlistaddress + 4 * i)
sehaddress += memory_image_base
print("SEHAddress = {}".format(hex(sehaddress)))
dbg.close()
运行这段代码,则可输出当前进程内所有启用SafeSEH
的内存地址空间,如下图所示;