海莲花glitch样本去混淆
2022-5-15 17:59:0 Author: mp.weixin.qq.com(查看原文) 阅读量:36 收藏


本文为看雪论坛优秀文章
看雪论坛作者ID:Tangdouren


去混淆思路

奇安信的报告《使用和海莲花相似混淆手法的攻击样本分析》[1]中分析了一个和APT32使用相同混淆方法的样本。本文根据奇安信的报告以及报告中提到的参考文章和代码[2]对该样本进行去混淆。
SHA256:bf3e495f43a6b333b10ae69667304cfd2c87e9100de9d31365671c7b6b93132e
 
如下图所示,cmp/test指令将数据段中存储的数据与立即数进行比较,下一条指令是条件跳转指令,根据比较结果来决定是否发生跳转。恶意代码通过这种方式来混淆控制流,影响分析人员进行逆向分析。

图 1-1 混淆代码
 
解决方法:

如果条件跳转指令不执行,将cmp/test +条件跳转指令替换成NOP指令。

如果条件跳转指令执行,将cmp/test到跳转地址之间的指令替换成NOP指令。

该dll当前载入的基址是0x10000000,与立即数(operation_2)进行比较的数据存储在地址operation_1(0x1007EE93),如果dll载入基址发生变化,则operation_1也会相应发生变化。为了使dll在基址发生改变时也能正确获取数据,使用dword_1007EE93的地址被记录在了重定位表中,当基址发生变化时,程序会根据重定位表中的地址修改operation_1。重定位表中保存了一大堆需要修正的代码的地址。
 
我们可以获取重定位表中存储的地址信息,通过判断该地址前面的指令是否为cmp/test来确定混淆指令的地址。模拟执行cmp/test、跳转指令后获取之后执行的指令地址。通过判断是否发生了跳转,将对应指令替换成NOP。


代码结构

2.1 获取重定位表中存储的需要修正的代码地址

使用python的pefile库获取重定位表中存储的RVA。

图 2-1 重定位表
reloc_table_num = pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_BASERELOC']  # 获取重定位表的VirtualAddress和Size reloc_table = pe.OPTIONAL_HEADER.DATA_DIRECTORY[reloc_table_num] reloc_table_rva = reloc_table.VirtualAddress reloc_table_size = reloc_table.Size print(f'重定位表RAV:{reloc_table_rva:#x},重定位表大小:{reloc_table_size:#x}')

重定位表由数个IMAGE_BASE_RELOCATION结构组成,每个结构由VirutalAddress(DWORD)、SizeOfBlock(DWORD)和TypeOffset(SizeOfBlock-8)组成。重定位数据2个字节一组,高4位是类型,低12位是地址。低12位加上VirutalAddress是RVA。以第一个数据0x3031为例,低12位是0x031,加上0x1000,是0x1031。
 

relocations = pe.parse_relocations_directory(reloc_table_rva, reloc_table_size)     reloc_data_rva = []     for i in relocations:         for j in i.entries:             # print(f'重定位数据RVA:{j.rva:#x}')             reloc_data_rva.append(j.rva)

parse_relocations_directory返回BaseRelocationData对象列表。BaseRelocationData有两个属性,struct和entries。struct是IMAGE_BASE_RELOCATION结构的VA和Size。entries是RelocationData对象列表,每一个RelocationData包含type和RVA,RVA是低12位加上VirutalAddress后的值。
class BaseRelocationData(DataContainer):     """Holds base relocation information.     struct:     IMAGE_BASE_RELOCATION structure     entries:    list of relocation data (RelocationData instances) """  class RelocationData(DataContainer):     """Holds relocation information.     type:       Type of relocation                 The type string can be obtained by                 RELOCATION_TYPE[type]     rva:        RVA of the relocation     """


图 2-2 BaseRelocationData的struct和RelocationData对象列表
 

图 2-3 重定位数据的RVA
 
reloc_data_rva列表中存储所有重定位数据RVA。

2.2 获取混淆指令所在地址

本部分代码引用自
https://github.com/levanvn/APT32_Deobfuscate/blob/master/Type2/Script/Type2_Deobfuscate.py
 
混淆指令有以下5种情况。





图 2-4 混淆指令
 
在默认操作数是32 位的OS 上,任何操作word 的指令都较操作dword 的指令长一个字节(Prefixes 0x66)。操作数前面的机器码长度是2到3字节。从使用重定位数据的地址往前3个字节或2个字节进行汇编,判断指令是否为cmp/test和跳转指令,如果是就获取到了混淆指令所在地址。
 
设置初值b为3,获取数据,如果往前3个字节开头是0x66,b减1,判断汇编代码,符合条件返回地址, reloc_data_rva - b – 1。如果开头不是0x66,b减1,判断往前2个字节的汇编代码,符合条件返回地址。
branch = ["JZ", "JP", "JO", "JS", "JG", "JB", "JA", "JL", "JE", "JNZ", "JNP", "JNO", "JNS", "JLE", "JNB", "JBE",               "JGE", "JNE", "JAE"]     b = 3     for i in range(3):         code = memory_data[reloc_data_rva - b: reloc_data_rva - b + 40]         if b == 3 and code[0] != 0x66:             b = b - 1             continue         b = b - 1         try:             ins = md.disasm(code, ImageBase + reloc_data_rva-b-1)              ins_1 = next(ins)             ins_2 = next(ins)             ins.close()         except StopIteration:             continue         if (ins_1.mnemonic == 'cmp' or ins_1.mnemonic == 'test') and ins_2.mnemonic.upper() in branch \                 and len(ins_1.operands) == 2 and ins_1.operands[0].type == X86_OP_MEM and ins_1.operands[1].type == X86_OP_IMM:             return return ins_1.address-0x10000000     return 0

2.3 模拟执行混淆指令

2.3.1 将文件映射到内存中

PE文件头中的FileAlignment定义了磁盘区块的对齐值,SectionAlignment定义了内存中区块的内存值。每一个区块从对齐值的倍数的偏移位置开始。

图 2-5 对齐值
 

图 2-6 文件映射到内存中的地址
 
pefile库中的函数get_memory_mapped_image()可以返回与PE文件的内存布局对应的数据
pe = pefile.PE(filename, fast_load=True) content = pe.get_memory_mapped_image() mu.mem_write(0x10000000, pe.get_memory_mapped_image())


2.3.2 定义hook_code函数

获取到混淆指令所在地址后,模拟执行3条指令,cmp/test、跳转指令和第3条指令,记录第3条指令的地址。
 
使用count对执行的指令进行计数,将count存储在esp寄存器中。如果执行完3条指令,则记录第3条指令地址,退出模拟执行。
instruction_3 = [] def hook_code(mu, address, size, userdata):     print(f'>>> Tracing instruction at {address:#x}, instruction size = {size:#x}')     r_esp = mu.reg_read(UC_X86_REG_ESP)     count = u32(mu.mem_read(r_esp + 4, 4))     print(f'count is {count}')     if count == 2:         instruction_3.append(address)         mu.emu_stop()         try:             exit()         except BaseException as e:             print(e)     count = count + 1 mu.mem_write(r_esp + 4, p32(count))  def simulate_execute(ins_addr_rva): mu.mem_write(r_esp + 4, p32(0))     mu.emu_start(ins_addr_rva + ImageBase, 0x100066E6)


2.3.3 调用函数获取跳转结果

跳转结果存储在instruction_3列表中。
reloc_data_rva = get_reloc_data_rva(pe) for rva in reloc_data_rva:     ins_addr_rva = get_intruction_start_rva(memory_mapped_image, rva, ImageBase)     if ins_addr_rva != 0:         simulate_execute(ins_addr_rva)


2.4 根据是否跳转执行替换相应指令

假设所有混淆指令的执行结果都是不跳转,通过顺序执行ins_3 = next(ins)的方式获取第3条指令的地址。将获取到的地址与模拟执行结果instruction_3列表中的地址进行比较,如果相等,则并未发生跳转,如果不相等则发生了跳转。顺序执行时有的地址无法进行汇编,将地址值赋值为0。
# 获取按顺序执行时第3条指令地址         code = memory_mapped_image[ins_addr_rva:ins_addr_rva+40]         ins = md.disasm(code, ImageBase + ins_addr_rva)          ins_1 = next(ins)         ins_2 = next(ins)          try:             ins_3 = next(ins)             ins_3_address = ins_3.address         except:             ins_3_address = 0         ins.close()

未发生跳转则将混淆指令test/cmp + 跳转指令赋值为0x90,发生跳转,则将混淆指令与目的跳转地址中间的数据全部赋值为0x90,中间的数据是垃圾数据,如果只将js等指令替换成“jmp 目的地址”,会影响程序的反汇编。
if instruction_3[count] == ins_3_address:             size = ins_1.size + ins_2.size             assembly = b'\x90' * size             patch(memory_mapped_image, ImageBase, ImageBase + ins_addr_rva, assembly)         else:             size = instruction_3[count] - ins_1.address             assembly = b'\x90' * size             patch(memory_mapped_image, ImageBase, ImageBase + ins_addr_rva, assembly)         count = count + 1

2.5 修复文件

使用函数set_bytes_at_rva(rva, data)修改PE 映像中的数据,并写入文件。
for section in pe.sections:     print(f'{section.Name}, VirtualAddress: {section.VirtualAddress:#x}, '           f'Size: {section.SizeOfRawData:#x}, 文件偏移: {section.PointerToRawData:#x}')     pe.set_bytes_at_rva(section.VirtualAddress,                         bytes(memory_mapped_image[section.VirtualAddress:section.VirtualAddress + section.SizeOfRawData]))  print('[+] Save to file ' + '1.bin') pe.write('1.bin')


完整代码

# _*_ coding: utf-8 _*_ import pefile import struct from capstone.x86 import * from capstone import * from unicorn import * from unicorn.x86_const import * from binascii import *   def u32(data):     return struct.unpack("I", data)[0]   def p32(num):     return struct.pack("I", num)  def patch(image, image_base, address, patch_data):     '''      :param image: memory_mapped_image 从入口点开始处的数据     :param image_base: 基址     :param address: imagebase+rva VA     :param patch_data:     :return:     '''     i = 0     for b in patch_data:         image[address - image_base + i] = b         i += 1     # 获取重定位表的序号 pefile.py 146行 def get_reloc_data_rva(pefile_struct):     '''      :param pefile_struct:     :return: 返回所有重定位数据的RVA列表     '''     reloc_table_num = pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_BASERELOC']      # 获取重定位表的VirtualAddress和Size     reloc_table = pe.OPTIONAL_HEADER.DATA_DIRECTORY[reloc_table_num]     reloc_table_rva = reloc_table.VirtualAddress     reloc_table_size = reloc_table.Size     print(f'重定位表RAV:{reloc_table_rva:#x},重定位表大小:{reloc_table_size:#x}')      # reloc_table由数个IMAGE_BASE_RELOCATION结构组成,每个结构由VirutalAddress(DWORD)、SizeOfBlock(DWORD)和TypeOffset(SizeOfBlock-8)组成     # parse_relocations_directory返回BaseRelocationData对象列表     relocations = pe.parse_relocations_directory(reloc_table_rva, reloc_table_size)     # 获取所有的重定位数据RVA     reloc_data_rva = []     for i in relocations:         # BaseRelocationData有两个属性,struct和entries。         # struct是IMAGE_BASE_RELOCATION结构的VA和Size。         # entries:    list of relocation data (RelocationData instances)         # RelocationData: type和RVA         # print(i.struct)         for j in i.entries:             reloc_data_rva.append(j.rva)     return reloc_data_rva  def get_intruction_start_rva(memory_data, reloc_data_rva, ImageBase):     '''      :param memory_data:  映射到内存中的文件数据     :param reloc_data_rva: 重定位数据的rva     :param ImageBase: ImageBase     :return: 指令的rva     '''      branch = ["JZ", "JP", "JO", "JS", "JG", "JB", "JA", "JL", "JE", "JNZ", "JNP", "JNO", "JNS", "JLE", "JNB", "JBE",               "JGE", "JNE", "JAE"]     b = 3     for i in range(3):         code = memory_data[reloc_data_rva - b: reloc_data_rva - b + 40]         if b == 3 and code[0] != 0x66:             b = b - 1             continue         b = b - 1         try:             ins = md.disasm(code, ImageBase + reloc_data_rva-b-1)              ins_1 = next(ins)             ins_2 = next(ins)             ins.close()         except StopIteration:             continue         if (ins_1.mnemonic == 'cmp' or ins_1.mnemonic == 'test') and ins_2.mnemonic.upper() in branch \                 and len(ins_1.operands) == 2 and ins_1.operands[0].type == X86_OP_MEM and ins_1.operands[             1].type == X86_OP_IMM:             return ins_1.address-0x10000000     return 0   filename = 'bf3e495f43a6b333b10ae69667304cfd2c87e9100de9d31365671c7b6b93132e' pe = pefile.PE(filename, fast_load=True)  memory_mapped_image = bytearray(pe.get_memory_mapped_image()) ImageBase = pe.OPTIONAL_HEADER.ImageBase  print('[+] Map PE') BASE = 0x10000000 STACK_ADDR = 0x400000 STACK_SIZE = 1024 * 1024  mu = Uc(UC_ARCH_X86, UC_MODE_32) mu.mem_map(BASE, 1024 * 1024) mu.mem_map(STACK_ADDR, STACK_SIZE)  r_esp = STACK_ADDR + STACK_SIZE // 2 mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + STACK_SIZE // 2)  # 将文件映射到内存中  mu.mem_write(0x10000000,pe.get_memory_mapped_image()) md = Cs(CS_ARCH_X86, CS_MODE_32) md.detail = True   instruction_3 = [] def hook_code(mu, address, size, userdata):     print(f'>>> Tracing instruction at {address:#x}, instruction size = {size:#x}')     r_esp = mu.reg_read(UC_X86_REG_ESP)     count = u32(mu.mem_read(r_esp + 4, 4))     print(f'count is {count}')      if count == 2:         instruction_3.append(address)         mu.emu_stop()         try:             exit()         except BaseException as e:             print(e)     count = count + 1     mu.mem_write(r_esp + 4, p32(count))  mu.hook_add(UC_HOOK_CODE, hook_code)    def simulate_execute(ins_addr_rva):     mu.mem_write(r_esp + 4, p32(0))      mu.emu_start(ins_addr_rva + ImageBase, 0x100066E6)  reloc_data_rva = get_reloc_data_rva(pe)  ins_addr_rva_all = [] count = 0 for rva in reloc_data_rva:     ins_addr_rva = get_intruction_start_rva(memory_mapped_image, rva, ImageBase)     if ins_addr_rva != 0:         ins_addr_rva_all.append(ins_addr_rva)         simulate_execute(ins_addr_rva)         # 获取按顺序执行时第3条指令地址         code = memory_mapped_image[ins_addr_rva:ins_addr_rva+40]         ins = md.disasm(code, ImageBase + ins_addr_rva)          ins_1 = next(ins)         ins_2 = next(ins)          try:             ins_3 = next(ins)             ins_3_address = ins_3.address         except:             ins_3_address = 0         ins.close()          if instruction_3[count] == ins_3_address:             size = ins_1.size + ins_2.size             assembly = b'\x90' * size             patch(memory_mapped_image, ImageBase, ImageBase + ins_addr_rva, assembly)         else:             size = instruction_3[count] - ins_1.address             assembly = b'\x90' * size             patch(memory_mapped_image, ImageBase, ImageBase + ins_addr_rva, assembly)         count = count + 1   for section in pe.sections:     print(f'{section.Name}, VirtualAddress: {section.VirtualAddress:#x}, '           f'Size: {section.SizeOfRawData:#x}, 文件偏移: {section.PointerToRawData:#x}')     pe.set_bytes_at_rva(section.VirtualAddress,                         bytes(memory_mapped_image[section.VirtualAddress:section.VirtualAddress + section.SizeOfRawData]))  print('[+] Save to file ' + '1.bin') pe.write('1.bin')
[1]. 使用和海莲花相似混淆手法的攻击样本分析
https://ti.qianxin.com/blog/articles/Obfuscation-techniques-similar-to-OceanLotus/
[2]. Type2_Deobfuscate.py
https://github.com/levanvn/APT32_Deobfuscate/blob/master/Type2/Script/Type2_Deobfuscate.py
[3]. pefile.py
https://github.com/erocarrera/pefile/blob/master/pefile.py
[4]. 菜鸟读capstone与keystone源码入门
https://bbs.pediy.com/thread-258473-1.htm
[5]. Unicorn引擎教程
https://bbs.pediy.com/thread-224330.htm#msg_header_h3_7

看雪ID:Tangdouren

https://bbs.pediy.com/user-home-826259.htm

*本文由看雪论坛 Tangdouren 原创,转载请注明来自看雪社区

# 往期推荐

1.反射式DLL注入实现

2.angr符号变量转LLVM IR

3.记录一次对某CMS漏洞挖掘

4.逆向角度看C++部分特性

5.CVE-2014-4113提权漏洞学习笔记

6.Go语言模糊测试工具:Go-Fuzz

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458444697&idx=2&sn=4cc3c292a210885abc6dc655f41991f5&chksm=b18fd31386f85a05842553a050a4f2b2bbdc3281d881ba974ad91a10d2ed4abfcbc2565c4e67#rd
如有侵权请联系:admin#unsafe.sh