可能是最简单一种免杀方式了
对于自己开发c2有启发意义
公众号:https://mp.weixin.qq.com/s/WSX-MxkV-8QUfsNULFM3Kg
在上一篇文章:【免杀】使用CobaltStrike的外置监听器绕过检测
我们实现了一个能通过external C2 来对杀软进行绕过的方法,那么为什么行呢?这里对通讯的流量进行分析。
首先是在spawnBeacon
需要运行一段teamserver发送过来的shellcode
1 2 3 4 5 6 7 8 9 10 11 void spawnBeacon (char *payload, DWORD len) { HANDLE threadHandle; DWORD threadId = 0 ; char *alloc = (char *)VirtualAlloc (NULL , len, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy (alloc, payload, len); threadHandle = CreateThread (NULL , NULL , (LPTHREAD_START_ROUTINE)alloc, NULL , 0 , &threadId); }
在IDA中下断点看看,这里的客户端是自己编译的,因此选择Debug模式,这样IDA能通过pdb文件实现源码级F5(其实也不算反编译)
看看这段汇编长啥样
第一个call rbx
翻译成C
这里从第三方客户端Dump下文件后查看
findPEFile
首先获得当前rsp的值,并且由于是小端序,应该从右向左读。从启示地址开始读取,直到读取到PE文件的DOS头的e_magic,接着再判断PE头,如果都成立,则返回DOS文件头的地址,所以我在上层函数中将返回的类型设置为了PIMAGE_DOS_HEAD
Part I. sub_180017948
在使用IDA打开Dump下来的bin文件时,IDA会将其默认解析为PE文件格式,说明这就是在反射式加载一个PE文件,所以要首先从LDR中加载出基本函数,例如LoadLibrary
、VirtualAlloc
等
首先是经典的InMemoryOrderModuleList
循环遍历寻找
这个循环看不懂,我把i
的类型设置为struct _LDR_DATA_TABLE_ENTRY *i
再来看看
既然是哈希,这个过程肯定是不可逆的,所以动态调试一下
发现值为:KERNEL32.DLL
可以得到该哈希,注意这里字串是wchar_t*
的宽字符类型,需要在 Options->String Literals 选择编码方式为unicode
尝试使用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 import osdef ror4 (value: int , bits: int ) -> int : value &= 0xFFFFFFFF return ((value >> bits) | (value << (32 - bits))) & 0xFFFFFFFF def hasher (filename: bytes )->int : v8 = 0 for ch in filename: v9 = ror4(v8, 13 ) v8 = (v9 + ord (ch)) & 0xFFFFFFFF v9 = ror4(v8, 13 ) v8 = (v9 + 0 ) & 0xFFFFFFFF return v8 if __name__ == '__main__' : folder_path = "C:\\Windows\\System32" files = os.listdir(folder_path) for file in files: if (file[-4 :]!=".dll" ): continue hash = hasher(file.upper()) if (hash ==0x6A4ABC5B ): print ("Found" , file, "hash is right: " , hex (hash ).upper()) break
这里使用的是C:\\Windows\\System32
目录下的DLL文件名
接着在静态我已经仅我最大努力了,可是还有很多不懂得
不过按照一般的流程就是找需要的导出函数了,动态调试看看
那么大概意思清楚了:读取dll的导出函数,进行自定义的哈希运算,找到符合的函数,从而加载函数,及GetProcAddress
的实现
1 2 3 do v6 = *v14++ + __ROR4__(v6, 13 ); while ( *v14 );
知道了是kernel32.dll的函数,我们自己测试看看
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 import osimport pefiledef ror4 (value: int , bits: int ) -> int : value &= 0xFFFFFFFF return ((value >> bits) | (value << (32 - bits))) & 0xFFFFFFFF def hasher (filename: bytes , wide_char: bool = True )->int : v8 = 0 for ch in filename: v9 = ror4(v8, 13 ) v8 = (v9 + ord (ch)) & 0xFFFFFFFF if (wide_char): v9 = ror4(v8, 13 ) v8 = (v9 + 0 ) & 0xFFFFFFFF return v8 def list_exported_functions (pe_path:str )->list : result = [] pe = pefile.PE(pe_path) if not hasattr (pe, 'DIRECTORY_ENTRY_EXPORT' ): print ("此文件没有导出表。" ) return for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols: name = exp.name.decode() if exp.name else "<no name>" address = hex (pe.OPTIONAL_HEADER.ImageBase + exp.address) ordinal = exp.ordinal result.append(name) return result if __name__ == '__main__' : folder_path = "C:\\Windows\\System32" files = os.listdir(folder_path) filename = "" for file in files: if (file[-4 :]!=".dll" ): continue hash = hasher(file.upper()) if (hash ==0x6A4ABC5B ): print ("Found" , file, "hash is right: " , hex (hash ).upper()) filename = file break if (filename != "" ): filepath = os.path.join(folder_path, filename) func_names = list_exported_functions(filepath) val = hasher(func_names[0 ], False ) print (func_names[0 ], hex (val))
修改一下一小段脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (filename != "" ): filepath = os.path.join(folder_path, filename) func_names = list_exported_functions(filepath) for name in func_names: v6 = hasher(name, False ) if v6 == 0xEC0E4E8E : print ("No.2" ,name,"\t" , f"0x{v6:02X} " ) if v6 == 0x7C0DFCAA : print ("No.1" ,name,"\t" , f"0x{v6:02X} " ) if v6 == 0x91AFCA54 : print ("No.4" ,name,"\t" , f"0x{v6:02X} " ) if v6 == 0x7946C61B : print ("No.5" ,name,"\t" , f"0x{v6:02X} " ) if v6 == 0x753A4FC : print ("No.3" ,name,"\t" , f"0x{v6:02X} " ) if v6 == 0xD3324904 : print ("No.0" ,name,"\t" , f"0x{v6:02X} " )
最后函数完成的v13
关于如何实现GetProcAddress
,可以参考之前关注发的文章:PE文件格式解析 的 《如何找到导入的函数和DLL-导入表》部分
sub_180017D38
主函数ReflectiveLoader
检查完成后进入sub_180017D38
根据之前的分析
sub_180017F88
ntHead->FileHeader.Characteristics
描述了IMAGE的特征。其中0x8000:反转单词的字节数。 此标志已过时。
根据参数修改下sub_180017F88
的类型就很好看了
动态调试发现没有走上面的复杂逻辑,或许是为了兼容性?
等效运行
1 VirtualAlloc (0 , SizeofImage, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
后续函数
基本就是反射式载入那一套,你依旧可以参考:https://github.com/stephenfewer/ReflectiveDLLInjection
最后跳转到LoaderFlags
或者AddressOfEntryPoint
,这里是dllmain,开始运行恶意DLL
复制PE文件DOS头
sub_180018218 ReflectSections
加载PE文件的Section到内存
sub_180018318 GetImportTable
从导入表开始导入相关函数和Dll
sub_1800185D8 DoRelocation
读取重定位表,进行重定位
小结
继续升入下去可以挖掘出cobaltstrike功能实现的方法和技巧。
这种方式的有效shellcode仅有跳转到ReflectLoader
之前的一小段汇编代码,所以看上“比较合法”,这样也大大提升了躲避检测的能力
同时这种方式可以使用之前提到过的反射式加载器加,不过需要自己维持一个ipc(名称一定要正确)和socket来负责传递数据