0.分析目标
恶意程序是否加壳?如果是,是常见的壳还是自写壳?
恶意程序使用什么网络通信手段?Winsock2、Wininet、COM、WSK甚至是自己实现的?
是否存在注入或者HOOK技术?如果存在,是什么?
是否存在对抗?反调试、反静态分析、反虚拟机?
是否存在API/DLL被加密?
是否存在字符串加密或混淆?
恶意程序使用了什么同步原语?
恶意程序使用了什么加密算法?
恶意程序使用了什么持久化手段?
是否存在ShellCode被注入进系统进程?
是否存在文件过滤驱动被安装?
如果安装了驱动,是否存在回调或者定时器?
1.信息收集 BAZAAR 报告:
TRIAGE报告:
沙箱报告:
DIE检测:
PE信息:
导入表信息
节表信息
导出表信息
总结:
样本来自qakbot家族,属于obama150僵尸网络。
有遍历进程以及读写其他进程的API被调用,可能存在注入。
可能是使用计划任务作为持久化。
可能将自己释放到temp目录下。
注入的目标进程可能是explorer.exe。
程序是由Borland Delphi编写的32位DLL。
导入表中未出现网络相关的API,可能被动态解析或者是加壳了。
存在不寻常的节区名。
存在一个导出函数,可以被用于动态调试。
2.脱壳 调试参数:
1 "C:\Windows\SysWOW64\rundll32.exe" C:\Users\mas\Desktop\mal_lab\test2\mas_2.bin,
eip指向oep时,在如下API设置断点:
VirtualAlloc( ) ret 0x10 // 检测自注入
WriteProcessMemory( ) // 根据上述信息,可能存在进程注入
NtResumeThread( ) // 为了避免失去控制权
设置好断点后,F9运行,每次断在VirtualAlloc返回位置时,将eax的值放入内存窗口中,按下F9观察内存中的内容。遇见异常按Shift + F9忽略继续执行。直到ResumeThread被断下时,此时会得到两个PE文件,如下:
第一个PE文件的节名异常,可能还没有脱完,将第二个PE所在的内存区域保存到文件rundll32_02900000.bin。
Trige报告中显示,可能存在注入,动态跟一下,在以下API设置断点:(这里没设置MapViewOfSection断点的原因是经过测试,断不下,API被重写了)
CreateProcessW( )
ResumeThread( )
重新运行,断点断在CreateProcessW(),创建msra.exe系统进程。
执行到这里直接在ZwMapViewOfSection( )下断断不到想要的结果,这里采用动态跟踪的方式,向下追踪代码,可以得到如下调用ZwMapViewOfSection的方式:
这里有个对抗技巧,根据eax + 0x14得到的地址根本不在ntdll中,而是在它释放的一个随即名dll中,相当于ntdll中的api被重写了一遍。
可以看见重写api列表如下:
函数地址来自a0f1f0bb.dll模块的.text段,这个模块就是上面得到那个第一个pe文件。
步过这个函数,可以看见msra.exe被挂起创建,并且多了一个132kb的可执行共享内存,如下:
映射内存可以直接由memcpy进行读写,这里不去追踪了,直接F9放行,断到ResumeThread( )函数位置:
此时使用ProcessHacker将即将执行的镜像内存dump下来,可以得知与之前dump文件是同一个pe的两种状态,如图所示:
手动修复PE:
将所有节表项的ra用va替换
计算相邻节表项之间的offset,用作rs和vs
将ImageBase修改为内存dump的起始位置
工具修复PE:
使用pe_unmapper修复一下内存中的dump,将其转换成未映射状态:
libpeconv/pe_unmapper at master · hasherezade/libpeconv (github.com)
1 2 // pe_unmapper.exe /in 输入文件 /base 映射基址 /out 输出文件 pe_unmapper.exe /in msra.exe_0x450000-0x21000.bin /base 00450000 /out msra.exe_0x450000-0x21000_fixed.bin
转换后的pe文件导入表可以正常解析:
3.逆向 3.0 IDA技巧
Ctrl + F5 反编译整个文件
Shift + F11 检查 mssdk_win7、ntapi_win7、ntddk_win7 是否存在,如果不存在,按INS添加。将vc32rtf添加到Shift + F5,分析驱动使用win10的库
运行 Flare Capa Explorer 和 FindCrypt 插件来收集信息
遇见申请大块堆内存,并且后续代码将该内存当数组处理的情况,可以创建大小一致结构体,将该堆内存空间指定为结构体类型,可以美化伪代码
多尝试使用枚举类型恢复API中的常量flag
3.1 自动化处理加密的字符串 3.1.1 Python脚本模拟算法 DllEntry向下不远处,会看到两个算法函数,如图所示:
decode_string_table函数实现如下:
参数列表:
1 j_decode_string_table (加密字符串表长度, 全局加密字符串表,全局解密key,未使用,字符串表索引)
j_decode_string_table函数实现如下:
该函数会根据传入的索引来解密相应的字符串,并申请堆内存来保存结果,返回堆内存指针。
经过测试,decode_string_table_2函数与decode_string_table功能一致,只不过block和key的地址变换了。
编写Python脚本模拟算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 from pydoc import docimport pefiledef decrypter (id , data_string, data_key ): size = 3660 index = id index2 = 0 if ( id < size ): flag = False while True : if (data_key[index % 0x5A ] == data_string[index]): break index += 1 if (index >= size): flag = True break if (flag == False ): index2 = index - id decopded_buf = '' index = 0 if (index2 != 0 ): while True : decopded_buf += chr ((data_string[id ]) ^ (data_key[id % 0x5A ])) id += 1 index += 1 if (index >= index2): break return decopded_bufdef extract_data (filepath ): pe = pefile.PE(filepath) for section in pe.sections: if '.data' in section.Name.decode(encoding = 'utf-8' ).rstrip('x00' ): return (section.get_data(section.VirtualAddress, section.SizeOfRawData))def calc_offsets (s_seg_start, x_start ): data_offset = hex (int (x_start, 16 ) - int (s_seg_start, 16 )) return data_offsetdef string_decrypter (id , data_seg_start, encrypted_string_addr, key_data_addr ): data_1 = b'' data_2 = b'' encrypted_string_addr_rel = calc_offsets(data_seg_start, encrypted_string_addr) key_data_addr_rel = calc_offsets(data_seg_start, key_data_addr) filepath = r"C:\\Users\\yuanmingfei\\Desktop\\mas_2\\mas_2_dump.bin " data_encoded_extracted_1 = extract_data(filepath) data_encoded_extracted_2 = extract_data(filepath) d1_off = 0x0 d2_off = 0x0 if (b'\x00\x00' in data_encoded_extracted_1[int (encrypted_string_addr_rel, 16 ): ]): d1_off = (data_encoded_extracted_1[int (encrypted_string_addr_rel, 16 ): ]).index(b'\x00\x00' ) if (b'\x00\x00' in data_encoded_extracted_2[int (key_data_addr_rel, 16 ): ]): d2_off = (data_encoded_extracted_2[int (key_data_addr_rel, 16 ): ]).index(b'\x00\x00' ) data_1 = data_encoded_extracted_1[int (encrypted_string_addr_rel, 16 ): int (encrypted_string_addr_rel, 16 ) + d1_off] data_2 = data_encoded_extracted_2[int (key_data_addr_rel, 16 ): int (key_data_addr_rel, 16 ) + d2_off] print (decrypter(id , data_1, data_2))def main (): string_decrypter(708 , '0x1001D000' , '0x1001D0B0' , '0x1001D050' )if __name__ == '__main__' : main()
3.1.2 编写IDA脚本实现自动注释 交叉引用后发现,大量位置使用该函数来解密字符串,编写IDA脚本实现自动对引用位置添加注释:
经过查看参数传递,发现大多数情况index均使用ecx以及push指令传参,并在距离call指令1-2个指令处,脚本遍历解密函数的全部交叉引用,并提取指令中的立即数作为index来解密字符串,再对当前位置添加注释。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 from pydoc import docimport pefileimport idaapiimport idautilsimport binasciidef decrypter (id , data_string, data_key ): size = 3660 index = id index2 = 0 if ( id < size ): flag = False while True : if (data_key[index % 0x5A ] == data_string[index]): break index += 1 if (index >= size): flag = True break if (flag == False ): index2 = index - id decopded_buf = '' index = 0 if (index2 != 0 ): while True : decopded_buf += chr ((data_string[id ]) ^ (data_key[id % 0x5A ])) id += 1 index += 1 if (index >= index2): break return decopded_bufdef extract_data (filepath ): pe = pefile.PE(filepath) for section in pe.sections: if '.data' in section.Name.decode(encoding = 'utf-8' ).rstrip('x00' ): return (section.get_data(section.VirtualAddress, section.SizeOfRawData))def calc_offsets (s_seg_start, x_start ): data_offset = hex (int (x_start, 16 ) - int (s_seg_start, 16 )) return data_offsetdef string_decrypter (id , encrypted_string_addr, key_data_addr ): data_1 = b'' data_2 = b'' encrypted_string_addr_ref = hex (int (encrypted_string_addr)) key_data_addr_ref = hex (int (key_data_addr)) for segment in idautils.Segments(): if '.data' == idc.get_segm_name(segment): data_seg_start = hex (int (idc.get_segm_start(segment))) encrypted_string_addr_rel = calc_offsets(data_seg_start, encrypted_string_addr_ref) key_data_addr_rel = calc_offsets(data_seg_start, key_data_addr_ref) filepath = r"C:\\Users\\yuanmingfei\\Desktop\\mas_2\\mas_2_dump.bin" data_encoded_extracted_1 = extract_data(filepath) data_encoded_extracted_2 = extract_data(filepath) d1_off = 0x0 d2_off = 0x0 if (b'\x00\x00' in data_encoded_extracted_1[int (encrypted_string_addr_rel, 16 ): ]): d1_off = (data_encoded_extracted_1[int (encrypted_string_addr_rel, 16 ): ]).index(b'\x00\x00' ) if (b'\x00\x00' in data_encoded_extracted_2[int (key_data_addr_rel, 16 ): ]): d2_off = (data_encoded_extracted_2[int (key_data_addr_rel, 16 ): ]).index(b'\x00\x00' ) data_1 = data_encoded_extracted_1[int (encrypted_string_addr_rel, 16 ): int (encrypted_string_addr_rel, 16 ) + d1_off] data_2 = data_encoded_extracted_2[int (key_data_addr_rel, 16 ): int (key_data_addr_rel, 16 ) + d2_off] decryped = decrypter(id , data_1, data_2) comment = ("string[%d]: %s" % (id , decryped)) return commentdef make_decompiler_comments (addr, comment ): c_function = idaapi.decompile(addr) treeloc_struct = idaapi.treeloc_t() treeloc_struct.ea = addr treeloc_struct.itp = idaapi.ITP_SEMI if c_function: c_function.set_user_cmt(treeloc_struct, comment) c_function.save_user_cmts()def comment_string_offset (encrypted_string_addr, key_data_addr, fun_offset ): str_function = idc.get_name_ea_simple(fun_offset) for k in idautils.CodeRefsTo(str_function, 0 ): p = idc.prev_head(k) my = idc.print_insn_mnem(p) if my in ('mov' , 'push' ): if my == 'mov' : if idc.get_operand_type(p, 1 ) == 5 : str_off = int (idc.print_operand(p, 1 )[:-1 ], 16 ) comment_string = string_decrypter(str_off, encrypted_string_addr, key_data_addr) idc.set_cmt(k, comment_string, 0 ) make_decompiler_comments(k, comment_string) if my == 'push' : if idc.get_operand_type(p, 0 ) == 5 : str_off = int (idc.print_operand(p, 0 )[:-1 ], 16 ) comment_string = string_decrypter(str_off, encrypted_string_addr, key_data_addr) idc.set_cmt(k, comment_string, 0 ) make_decompiler_comments(k, comment_string) else : j = idc.prev_head(p) my2 = idc.print_insn_mnem(j) if my2 in ('mov' , 'push' ): if my2 == 'mov' : if idc.get_operand_type(j, 1 ) == 5 : str_off = int (idc.print_operand(j, 1 )[:-1 ], 16 ) comment_string = string_decrypter(str_off, encrypted_string_addr, key_data_addr) idc.set_cmt(k, comment_string, 0 ) make_decompiler_comments(k, comment_string) if my2 == 'push' : if idc.get_operand_type(j, 0 ) == 5 : str_off = int (idc.print_operand(j, 0 )[:-1 ], 16 ) comment_string = string_decrypter(str_off, encrypted_string_addr, key_data_addr) idc.set_cmt(k, comment_string, 0 ) make_decompiler_comments(k, comment_string) comment_string_offset(0x1001D5A8 , 0x1001E3F8 , "decode_string_table_1_1" ) comment_string_offset(0x1001D5A8 , 0x1001E3F8 , "decode_string_table_1_2" ) comment_string_offset(0x1001D0B0 , 0x1001D050 , "decode_string_table_2_1" ) comment_string_offset(0x1001D0B0 , 0x1001D050 , "decode_string_table_2_2" )print ("Done!" )
执行结果如下:
只有少部分使用寄存器间接传参的,其他的情况均可正常解析。
使用到的ida_api如下:
ida_idaapi API documentation (hex-rays.com)
idautils API documentation (hex-rays.com)
idc API documentation (hex-rays.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import idaapiimport idautilsfor segment in idautils.Segments(): if '.data' == idc.get_segm_name(segment): data_seg_start = hex (int (idc.get_segm_start(segment))) idc.get_name_ea_simple(func_name) idautils.CodeRefsTo(addr, 0 ) idc.prev_head(addr) idc.print_insn_mnem(addr) idc.get_operand_type(addr, 1 ) idc.print_operand(addr, 1 ) idc.set_cmt(addr, comment, 0 )def make_decompiler_comments (addr, comment ): c_function = idaapi.decompile(addr) treeloc_struct = idaapi.treeloc_t() treeloc_struct.ea = addr treeloc_struct.itp = idaapi.ITP_SEMI if c_function: c_function.set_user_cmt(treeloc_struct, comment) c_function.save_user_cmts()
3.2 处理动态解析API 3.2.1 整体逻辑分析 字符串解密函数不远处的sub_1000E369函数就是用来解析API的,代码如下:
pe文件可能包括导入dll,这个函数根据字符串解密结果来解析指定dll,并在堆中构建新的IAT表,将地址返回。
整个解析过程如下图所示:
3.2.2 Hash函数名还原 经过分析,样本中有一块全局数据区,存放的是所有经过hash化的导入函数名,每个hash串长度为4字节,hash算法为crc32,代码如下:
hash使用的key为0x218FE95B,代码如下:
其中的0x218FE95B为hash算法用到的xor key,使用Hash DB插件来自动化解析hash数据,首先在插件中设置xor key,如下图所示:
将参数的data转变成如下格式:
对着4字节hash串,右键使用Hash DB来判断算法,如图所示:
选择算法后,再对这个hash串右键使用Hash DB的Lookup功能来解析,之后对连续的hash串使用Hash DB的Scan IAT即可,Hash DB会自动创建枚举类型,手动将名称全部恢复,如下图所示:
对着ma_decode_api函数按 ‘X’ 查看交叉引用,将其他位置的hash名称也恢复出来。
3.2.3 PE遍历导出表 首先是初始化PE相关的表指针,代码如下:
PE相关的IDA内置结构如下:
Sunshine’s Homepage - PE file format (sunshine2k.de)
_IMAGE_DOS_HEADER
_IMAGE_NT_HEADERS
_IMAGE_DATA_DIRECTORY
_IMAGE_EXPORT_DIRECTORY
_IMAGE_OPTIONAL_HEADER
_IMAGE_SECTION_HEADER
_IMAGE_IMPORT_DESCRIPTOR
遍历导出名字表,计算并比对hash,将得到的索引用来解析导出表三层结构获取导出函数地址,代码如下:
根据导出函数地址判断是否为导出转发函数,代码如下:
PE导出转发函数:
在一些系统dll中,如Kenel32.dll中的AddVectoredExceptionHandler()函数,实际上这个函数的真正代码为ntdll中的RtlAddVectoredExceptionHandler(),如下图所示:
此时Function RVA指向pe导出表的用于保存函数名字的内存区 ,并且数据格式如下:
1 NTDLL.RtlAddVectoredExceptionHandler
此时想要获取这个地址,就需要加载ntdll.dll并调用GetProcAddress来获取。
解析转发dll,加载,获取导出函数地址,代码如下:
3.2.4 生成IAT表 函数将获取到的所有导入函数地址,用一块堆内存来保存(重构IAT表),并将堆首地址返回给上一级,函数如下:
增强伪代码可读性,为之前得到的全局hash序列创建结构体,如图所示:
并将接收指针的全局变量全部重命名,并将类型修改为指定的结构体指针,如图所示:
此时去到全局变量引用位置,代码可读性大大提升,对比如下:
修复前:
修复后:
3.3 加密算法识别 3.3.1 Mersenne Twister 从一个随机字符串生成函数中识别一下MT伪随机数生成算法,函数(sub_1000B82D)代码如下:
MT算法由几个部分组成:根据seed初始化状态函数、状态传递函数、根据状态生成随机数函数。
初始化函数如下:
状态传递与生成随机数函数合在一起了,代码如下:
3.3.2 RC4 在追网络通信ip地址来源的时候,发现了rc4相关的函数,其中初始化s_box函数代码如下:
rc4是异或加密,加密解密函数为同一个,代码如下:
3.3.3 SHA1 在追踪网络通信的路上,可以发现存在SHA1算法函数,可以根据初始化的hash值来识别算法,循环分组处理代码:
80轮分组运算如下:
3.4 持久化分析 3.4.1 定位思路 对着解密出来的函数查看交叉引用,可以看到一些关键字符串,如下图:
定位过去依次分析,包括两种驻留方式:修改注册表Run键值和添加计划任务。
3.4.2 注册表 方式一: 使用Windows API修改注册表项
方式二: 启动reg.exe携带参数修改注册表
3.4.3 计划任务 创建schtasks.exe进程添加计划任务,代码如下:
3.6 C2配置提取 根据rc4_encode()函数的交叉引用向上追,会找到加载资源相关的函数,代码如下:
可以看见加载的资源id为5812,得到的powershell的字符串后续作为解密用。
查看这个ma_load_resource()函数的交叉引用会得到还有个位置引用它,资源id为3719。其中ma_liad_resource()函数内部如下:
对调用加载3719号资源的函数查看交叉引用,可以得到如下代码:
很是在格式化ip地址+端口的情况,在继续追加载资源后的ma_resource_decryptor()函数,最终会得到如下解密代码:
使用”\System32\WindowsPowerShell\v1.0\powershell.exe”作为明文生成rc4_key,再使用rc4对资源数据进行解密,代码如下:
编写Python3解密脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import binasciiimport pefileimport ipaddressfrom Crypto.Cipher import ARC4from Crypto.Hash import SHA1 key = b'\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' sha1_key = SHA1.new(data=key).digest()def data_decryptor (key_data, data ): data_cipher = ARC4.new(key_data) decrypted_config = data_cipher.decrypt(data) return decrypted_configdef extract_resource (filename, res_identifaction ): extracted_data = b"" pe = pefile.PE(filename) for resource in pe.DIRECTORY_ENTRY_RESOURCE.entries: if hasattr (resource, 'directory' ): for res_id in resource.directory.entries: if hasattr (res_id, 'name' ): if (res_id.name): if (str (res_id.name)) == str (res_identifaction): offset = res_id.directory.entries[0 ].data.struct.OffsetToData resid_size = res_id.directory.entries[0 ].data.struct.Size extracted_data = pe.get_memory_mapped_image()[offset:offset+resid_size] return extracted_datadef main (): filename = r"C:\\Users\\yuanmingfei\\Desktop\\mas_2\\mas_2_dump.bin" resource_data = extract_resource(filename, 5812 ) decrypted_data = data_decryptor(sha1_key, resource_data)[20 :] print ('DECRYPTED BOTNET AND CAMPATIGN ID: ' , end = '' ) print ("\n" +34 *'-' ) print (decrypted_data.decode('latin1' )) resource_data = extract_resource(filename, 3719 ) decrypted_data = data_decryptor(sha1_key, resource_data) resource_item = decrypted_data[21 :] print ("C2 IP ADDRESS LIST:" ) print (30 *'-' ) k = 0 i = 0 while (k < len (resource_item)): ip_item = resource_item[k:k+4 ] ip_port = resource_item[k+4 :k+6 ] print ("IP[%d]: %s" % (i, ipaddress.IPv4Address(ip_item)), end='' ) print (int (binascii.hexlify(ip_port), 16 )) k = k + 7 i = i + 1 if __name__ == '__main__' : main()
解密结果如下:
共计150条C2信息
3.7 WMI分析 分析sub_1000D6D0()函数,代码如下:
可以得到以下信息:
1 2 rclsid: 4590F811-1D3A-11D0-1F89-00AA004B2E24 riid: DC12A687-737F-11CF-884D-00AA004B2E24
Google搜索riid,可以得知ppv类型为IWbemLocator,修改类型,按下F5刷新,伪代码如下:
调用ConnectServer()函数将对象绑定到 ROOT\CIMV2 命名空间,并设置身份验证信息,将得到的指针返回上一级。