前言

Cobalt Strike已经成了目前工作中经常用渗透工具了,通常我们会通过写一下插件来武装自己的Cobalt Strike,比如我们会用bexecute_assembly来对自己编写的Csharp进行内存加载实现不落地。那么其他语言的呢?同样也提供了bdllspawn来反射DLL。本文章主要讲的就是利用反射DLL来武装自己的Cobalt Strike。

C++ ReflectiveDLL

首先将C/C++编写的程序如何进行ReflectiveDLL,Cobalt Strike的bdllspawn是基于项目ReflectiveDLLInjection实现的。我们只需要把C++编写的功能写到ReflectiveDll.c里即可,这里参考倾旋师傅的文章和工具。改写ReflectiveDll.c来实现传参。

参数转换

引用与定义
1
2
3
4
5
6
7
8
9
10
#include "ReflectiveLoader.h"
#include <string>
#include <shellapi.h>
#pragma comment(lib, "Shell32.lib")

std::string szargs;
std::wstring wszargs;
std::wstring wsHostFile;
int argc = 0;
LPWSTR* argv = NULL;
参数类型转换

ReflectiveDll.c里是通过DLLMain函数的lpReserved来当做参数传递,我们可以做一个类型的转换,将lpReserved转换成命令行参数格式。

1
2
3
szargs = (PCHAR)lpReserved;
wszargs = StringToWString(szargs);
argv = CommandLineToArgvW(wszargs.data(), &argc);

功能编写

在转换参数后,我们就可以把一些C++功能代码写入,我这就简单编写一个参数输出功能,进行测试。

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
hAppInstance = hinstDLL;
printf("C++ ReflectiveDLL\n");

/* print some output to the operator */
if (lpReserved != NULL) {
szargs = (PCHAR)lpReserved;
wszargs = StringToWString(szargs);
argv = CommandLineToArgvW(wszargs.data(), &argc);
}
if (argv == NULL) {
printf("[+] Error Arguments ! \n");
break;
}
printf("[+] Args Count : %d \n", argc);
for (size_t i = 0; i < argc; i++)
{
wprintf(TEXT("[%d] %s \n"), i, argv[i]);
}

/* flush STDOUT */
fflush(stdout);

/* we're done, so let's exit */
ExitProcess(0);
break;

Aggressor Script

编译ReflectiveDll.c得到一个dll文件,然后编写一个Aggressor Script脚本来加载它。

Aggressor Script脚本提供了一些关于反射DLL的接口:https://cobaltstrike.com/aggressor-script/functions.html#bdllspawn

1
2
3
4
alias hello {
$args = substr($0, 6);
bdllspawn($1, script_resource("reflective_dll.dll"),$args, "test dll", 5000, false);
}

测试效果

这样我们就实现了将c++编写的dll通过ReflectiveDll来实现不落地传参执行了。

image-20200708130204789

Golang ReflectiveDLL

Golang也成了现在一些安全工作者用得比较多的一种语言了,使用Golang开发的安全工具也越来越多,所以我们也可以通过ReflectiveDLL来对Golang程序进行利用。

这里参考WBGlIl师傅的项目go-ReflectiveDLL和国外大佬的文章Weaponizing your favorite Go program for Cobalt Strike

特别感谢一下WBGlIl师傅,在我遇到问题的时候给予我的帮助~

main.go

参考WBGlIl师傅的项目,将main.go改成一个传入参数并输出参数的功能,并将test函数设置为导出函数。

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
package main

import "C"

import (
"fmt"
"os"
gsq "github.com/kballard/go-shellquote"
)



//export test
func test(arg string) {

args, err := gsq.Split(arg)
if err == nil {
fmt.Println("Golang ReflectiveDLL")
os.Args = args
fmt.Printf("Args Count %d\n",len(os.Args))
for i := 0; i < len(os.Args); i++ {
fmt.Printf("[%d] %s\n",i,os.Args[i])
}
}
}

func main() {

}

dllmain.c

参考老外文章将lpReserved转换为 GoString,传入到dll的导出函数test里。

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
#include "dllmain.h"
#include<Windows.h>


BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpReserved) // reserved
{
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
{
GoString goArgs = {0};
if(lpReserved != NULL){
goArgs.p = (char*)lpReserved;
goArgs.n = strlen(lpReserved);
}else{
goArgs.p = "";
goArgs.n = 0;
}
test(goArgs);
}
break;
case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
}
return TRUE; // Successful.
}

然后运行WBGlIl师傅的项目里的x64.bat来进行编译得到dll文件,但是Golang有一个最大的缺点就是编译出来的文件特别大,这里一个简单的输入输出工具生成的dll就有差不多2M。而在Cobalt Strike限制了反射DLL的DLL大小必须在1M以内,所以这里我们不能用Cobalt Strike进行测试。

Inject.c修改

​ ReflectiveDLLInjection项目中的inject是不能给DLL进行传参的,所以我们这里需要修改代码来进行传参。

注入当前进程

如果对当前进程进行注入可以修改代码,将输入参数传入到LoadRemoteLibraryR函数的第四个参数即可。

1
2
LPVOID lpParameter = argv[3];
hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, lpParameter);

image-20200708152051963

注入到其他进程

如果注入到其他进程的话,需要将参数写入到目标进程得到一个参数指针,再讲这个指针传入LoadRemoteLibraryR函数。

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
lpRemoteMem = arg;
// 申请内存
argSize = strlen(arg);
lpRemoteMem = VirtualAllocEx(hProcess, NULL, argSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!lpRemoteMem)
{
BREAK_WITH_ERROR("\t\t[!] FAILED to allocate memory in process.\n");
CloseHandle(hProcess);
break;
}

printf("[+] Memory allocated at : 0x%d in process %d\n", lpRemoteMem, dwProcessId);
printf("[+] Attempting to write parameter in process %d \n", dwProcessId);


//将参数写入目标进程
bWriteSuccess = WriteProcessMemory(hProcess, lpRemoteMem, arg, argSize, &numBytes);

if (!bWriteSuccess)
{
printf("[!] FAILED to write parameter. Wrote %d bytes instead of %d bytes.\n ", numBytes ,argSize);

CloseHandle(hProcess);
break;
}
printf("[+] Wrote parameter in remote process %d memory.\n", dwProcessId);

//将参数指针传入
hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, lpRemoteMem);
if( !hModule )
BREAK_WITH_ERROR( "Failed to inject the DLL" );

printf( "[+] Injected the '%s' DLL into process %d.\n", cpDllFile, dwProcessId);

因为这里是注入到了其他进程里,所以当前进程是没有输出的。如果需要输出的话,可以修改DLL文件和Inject.c,即当前进程和DLL注入的进程之间用命名管道进行通信。这里以C++写的DLL为例。

inject.c
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
//接收

srand(time(NULL));

char buf[256] = "";
DWORD rlen = 0;
HANDLE hPipe = CreateNamedPipe(
TEXT("\\\\.\\Pipe\\mypipe"), //管道名
PIPE_ACCESS_DUPLEX, //管道类型
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, //管道参数
PIPE_UNLIMITED_INSTANCES, //管道能创建的最大实例数量
0, //输出缓冲区长度 0表示默认
0, //输入缓冲区长度 0表示默认
NMPWAIT_WAIT_FOREVER, //超时时间
NULL); //指定一个SECURITY_ATTRIBUTES结构,或者传递零值.
if (INVALID_HANDLE_VALUE == hPipe)
{
printf("[+] Create Pipe Error(%d)\n", GetLastError());
}
else
{
printf("[+] Create Pipe Success\n");
printf("[+] Waiting For Client Connection...\n");
if (ConnectNamedPipe(hPipe, NULL) == NULL) //阻塞等待客户端连接。
{
printf("[+] Connection failed!\n");
}
else
{
printf("[+] Connection Success!\n");
}
printf("[+] Data From Pipe :\n\n");
while (1)
{
if (ReadFile(hPipe, buf, 256, &rlen, NULL)) //接受客户端发送过来的内容
{
printf("\t%s", buf);
}
else
{
printf("\n[+] Read Data From Pipe End!\n");
break;
}
}
CloseHandle(hPipe);//关闭管道
}
ReflectiveDll.cpp
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
52
53
54
55
56
//利用命名管道传输
srand(time(NULL));
DWORD wlen = 0;

BOOL bRet = WaitNamedPipe(TEXT("\\\\.\\Pipe\\mypipe"), NMPWAIT_WAIT_FOREVER);

if (!bRet)
{
printf("connect the namedPipe failed!\n");
break;
}

HANDLE hPipe = CreateFile( //管道属于一种特殊的文件
TEXT("\\\\.\\Pipe\\mypipe"), //创建的文件名
GENERIC_READ | GENERIC_WRITE, //文件模式
0, //是否共享
NULL, //指向一个SECURITY_ATTRIBUTES结构的指针
OPEN_EXISTING, //创建参数
FILE_ATTRIBUTE_NORMAL, //文件属性(隐藏,只读)NORMAL为默认属性
NULL); //模板创建文件的句柄


hAppInstance = hinstDLL;
char buf[256] = "";
sprintf(buf, "C++ ReflectiveDLL\n");
WriteFile(hPipe, buf, sizeof(buf), &wlen, 0); //向服务器发送内容

/* print some output to the operator */
if (lpReserved != NULL) {
szargs = (PCHAR)lpReserved;
wszargs = StringToWString(szargs);
argv = CommandLineToArgvW(wszargs.data(), &argc);
}
else {
sprintf(buf, "Hello from test.dll. There is no parameter\n");
WriteFile(hPipe, buf, sizeof(buf), &wlen, 0); //向服务器发送内容
}
if (argv == NULL) {
sprintf(buf, "[+] Error Arguments ! \n");
WriteFile(hPipe, buf, sizeof(buf), &wlen, 0); //向服务器发送内容
break;
}
sprintf(buf, "[+] Args Count : %d \n", argc);
WriteFile(hPipe, buf, sizeof(buf), &wlen, 0); //向服务器发送内容
for (size_t i = 0; i < argc; i++)
{
sprintf(buf, "[%d] %s \n", i, argv[i]);
WriteFile(hPipe, buf, sizeof(buf), &wlen, 0); //向服务器发送内容
}
CloseHandle(hPipe);//关闭管道

/* flush STDOUT */
fflush(stdout);

/* we're done, so let's exit */
ExitProcess(0);

这样就可以实现读取注入目标进程的输出了。

image-20200708152800733

示例代码

https://github.com/uknowsec/ReflectiveDLLInjection-Notes

结语

WBGlIl师傅的帮助下,学习了ReflectiveDLL。本意是想用于Cobalt Strike插件的开发,但是Golang编译后的文件过大,导致Cobalt Strike并不能进行加载反射。有请教过别的师傅如何解决这个问题,方案好像都必须得落地待反射的Golang DLL,效果并不是太好。同时在最近新发布的Cobalt Strike4.1中有了一个新功能Beacon Object File (BOF),他可以解决文件过大的问题。

在官方文档help-beacon-object-files有如下的一段话:

BOFs are also very small. A UAC bypass privilege escalation Reflective DLL implementation may weigh in at 100KB+. The same exploit, built as a BOF, is <3KB. This can make a big difference when using bandwidth constrained channels, such as DNS.

所以现在就差一个Cobalt Strike4.1啦~

References

[1] bdllspawn: https://cobaltstrike.com/aggressor-script/functions.html#bdllspawn
[2] ReflectiveDLLInjection: https://github.com/stephenfewer/ReflectiveDLLInjection
[3] 倾旋: https://payloads.online/archivers/2020-03-02/1
[4] WBGlIl: https://wbglil.github.io/
[5] go-ReflectiveDLL: https://github.com/WBGlIl/go-ReflectiveDLL
[6] Weaponizing your favorite Go program for Cobalt Strike: https://ethicalchaos.dev/2020/01/26/weaponizing-your-favorite-go-program-for-cobalt-strike/
[7] help-beacon-object-files: https://www.cobaltstrike.com/help-beacon-object-files